# REPRO-2026-00288: Horde VFS < 3.0.1 OS command injection via Horde_Vfs_Smb driver ## Summary Status: published Severity: high Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00288 CVE: CVE-2026-60102 ## Package Name: horde/vfs Ecosystem: github Affected: < 3.0.1 Fixed: 3.0.1 ## Root Cause # Root Cause Analysis: CVE-2026-60102 ## Summary Horde VFS before 3.0.1 builds a shell command string in `Horde_Vfs_Smb::_command()` and executes it with `proc_open()`. The `_escapeShellCommand()` helper only escapes semicolons and backslashes, leaving command substitution (`$(...)`) and backticks intact. A crafted file or folder name containing `$(...)` is therefore interpolated into a double-quoted shell context and executed by `/bin/sh -c` before the configured `smbclient` binary is ever invoked. We confirmed this by calling the real `Horde_Vfs_Smb::createFolder()` method with a folder name of `$(id > /tmp/horde_vfs_smb_poc_folder).dir`; the vulnerable commit creates the marker file with `id` output, while the fixed commit leaves the marker untouched and passes the literal payload as an argument to `smbclient`. ## Impact - **Package/component:** Horde VFS, specifically the `Horde_Vfs_Smb` driver (`lib/Horde/Vfs/Smb.php`). - **Affected versions:** All versions before commit `41f74b4acfc144e09013d04dd121e0a5da808361` (released as `v3.0.1`). - **Risk level:** High. An authenticated caller can inject arbitrary shell commands through normal VFS operations such as file upload, create folder, rename, delete, or read paths. - **Consequences:** Full OS command execution as the user running the PHP process. ## Impact Parity - **Disclosed/claimed maximum impact:** Code execution (OS command injection) via `proc_open()` / `/bin/sh -c`. - **Reproduced impact from this run:** We demonstrated arbitrary command execution by causing the shell to run `id > /tmp/horde_vfs_smb_poc_folder` and captured the resulting `uid`/`gid` output. - **Parity:** `full` for the `createFolder()` path; the same quoting weakness is present in all VFS operations that build the `smbclient -c` command string. - **Not demonstrated:** No remote authentication bypass was needed; the reproduction assumes an already authenticated VFS caller as described in the CVE. ## Root Cause In the vulnerable code (`lib/Horde/Vfs/Smb.php` before `41f74b4^`), `_command()` concatenates user-supplied paths/names into a single string: ```php $fullcmd = $this->_params['smbclient'] . ' "//' . $this->_params['hostspec'] . '/' . $share . '"' . ' "-U' . $this->_params['username'] . '"' . ' -D "' . $path . '"' . ' -c "'; foreach ($cmd as $c) { $fullcmd .= $c . ";"; } $fullcmd .= '"'; return $this->_execute($fullcmd); ``` `_execute()` passes the string directly to `proc_open()`, which invokes the configured shell (`/bin/sh -c`) when the command is not an array. The `_escapeShellCommand()` helper only escapes `;` and `\`, so `$`, backticks, parentheses, and quotes remain active. A folder name like `$(id > /tmp/horde_vfs_smb_poc_folder).dir` is first quoted in a double-quoted `mkdir "..."` segment, but the shell still expands `$(...)` before running `smbclient`. The fix (`41f74b4acfc144e09013d04dd121e0a5da808361`) replaces the shell string with an `argv` array passed to `proc_open()` and adds `_quoteSmbArg()` to escape single and double quotes inside the `smbclient -c` mini-language. Because `proc_open()` with an array calls `execvp()` directly, no shell is involved and shell metacharacters lose their special meaning. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh` from the bundle directory. 2. The script checks out the vulnerable commit (`41f74b4^`) and the fixed commit (`41f74b4`) of `horde/Vfs` into a cached repository. 3. It installs PHP and Composer, then runs `composer install` for each commit. 4. It creates a fake `smbclient` that logs arguments and exits 0. 5. It runs a PHP harness that instantiates `Horde_Vfs_Smb` with the fake `smbclient` and calls `createFolder('', '$(id > /tmp/horde_vfs_smb_poc_folder).dir')`. 6. Expected evidence: on the vulnerable commit, `/tmp/horde_vfs_smb_poc_folder` is created and contains the `id` output; on the fixed commit, the marker is not created and the fake `smbclient` receives the literal payload. ## Evidence - `bundle/logs/reproduction_steps.log` — overall run summary, commit SHAs, and captured fake `smbclient` arguments. - `bundle/logs/vulnerable.log` — shows `Marker exists: YES` with `uid=1000(vscode) ...`. - `bundle/logs/fixed.log` — shows `Marker exists: NO`. - `bundle/logs/vulnerable_smbclient.log` — the fake `smbclient` receives `mkdir ".dir";` because the shell already executed `$(id > ...)` and substituted an empty string. - `bundle/logs/fixed_smbclient.log` — the fake `smbclient` receives the literal `mkdir "$(id > /tmp/horde_vfs_smb_poc_folder).dir";`, confirming no shell expansion occurred. - `bundle/repro/artifacts/fake_smbclient.sh` — the fake `smbclient` used as the command target. - `bundle/repro/artifacts/harness.php` — the PHP harness that drives the real library path. Key excerpts: ``` === vulnerable (994f4a46fd76ea44eae2fe839216bb273ce49080) === Marker exists: YES Marker content: uid=1000(vscode) gid=1000(vscode) groups=1000(vscode),969(969) ``` ``` === fixed (41f74b4acfc144e09013d04dd121e0a5da808361) === Marker exists: NO ARG //127.0.0.1/share ARG -c ARG mkdir "$(id > /tmp/horde_vfs_smb_poc_folder).dir"; ``` ## Recommendations / Next Steps - **Upgrade guidance:** Upgrade to Horde VFS `v3.0.1` or any release containing commit `41f74b4acfc144e09013d04dd121e0a5da808361`. - **Fix approach:** Avoid shell-string execution; pass the `smbclient` command as an `argv` array to `proc_open()` and quote names inside the `smbclient -c` mini-language rather than relying on shell quoting. - **Testing recommendations:** Add unit tests for `createFolder`, `write`, `rename`, `delete`, and `readFile` with payloads containing `$(...)`, backticks, pipes, semicolons, and quotes to prevent regression. ## Additional Notes - **Idempotency:** The reproduction script was run twice consecutively with identical results; both the vulnerable and fixed commits behaved consistently. - **Limitations:** The proof uses the library API (`function_call`) with a fake `smbclient` because the CVE claims the vulnerability is reachable through authenticated VFS operations. The actual production entry point in a Horde application would call the same `Horde_Vfs_Smb` methods after authentication. - **Environment:** Ubuntu 26.04, PHP 8.5, Composer 2.9, `horde/Vfs` repository at `https://github.com/horde/Vfs`. ## Reproduction Details Reproduced: 2026-07-14T04:21:11.529Z Duration: 770 seconds Tool calls: 135 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00288 pruva-verify CVE-2026-60102 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00288&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00288/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-60102 - Source: https://github.com/horde/Vfs/pull/10, https://github.com/horde/Vfs/commit/41f74b4acfc144e09013d04dd121e0a5da808361, https://github.com/horde/Vfs/releases/tag/v3.0.1, https://www.cve.org/CVERecord?id=CVE-2026-60102, https://nvd.nist.gov/vuln/detail/CVE-2026-60102 ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 6840 bytes) - bundle/repro/rca_report.md (analysis, 6351 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 6536 bytes) - bundle/vuln_variant/rca_report.md (analysis, 7740 bytes) - bundle/artifact_promotion_manifest.json (other, 13549 bytes) - bundle/artifact_promotion_report.json (other, 13567 bytes) - bundle/vuln_variant/source_identity.json (other, 753 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 2236 bytes) - bundle/logs/reproduction_steps.log (log, 820 bytes) - bundle/logs/vulnerable.log (log, 267 bytes) - bundle/logs/fixed.log (log, 187 bytes) - bundle/repro/validation_verdict.json (other, 713 bytes) - bundle/repro/runtime_manifest.json (other, 780 bytes) - bundle/logs/vulnerable_smbclient.log (log, 144 bytes) - bundle/logs/fixed_smbclient.log (log, 192 bytes) - bundle/repro/artifacts/fake_smbclient.sh (other, 159 bytes) - bundle/repro/artifacts/harness.php (other, 1162 bytes) - bundle/logs/vuln_variant/variant_run.log (log, 6062 bytes) - bundle/vuln_variant/validation_verdict.json (other, 1009 bytes) - bundle/vuln_variant/variant_manifest.json (other, 2792 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 6033 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 1666 bytes) - bundle/logs/vuln_variant/createFolder_vulnerable.log (log, 302 bytes) - bundle/logs/vuln_variant/createFolder_fixed.log (log, 306 bytes) - bundle/logs/vuln_variant/deleteFile_vulnerable.log (log, 298 bytes) - bundle/logs/vuln_variant/deleteFile_fixed.log (log, 302 bytes) - bundle/logs/vuln_variant/writeData_vulnerable.log (log, 296 bytes) - bundle/logs/vuln_variant/writeData_fixed.log (log, 300 bytes) - bundle/logs/vuln_variant/rename_vulnerable.log (log, 290 bytes) - bundle/logs/vuln_variant/rename_fixed.log (log, 294 bytes) - bundle/logs/vuln_variant/readFile_vulnerable.log (log, 294 bytes) - bundle/logs/vuln_variant/readFile_fixed.log (log, 298 bytes) - bundle/logs/vuln_variant/deleteFolder_vulnerable.log (log, 302 bytes) - bundle/logs/vuln_variant/deleteFolder_fixed.log (log, 306 bytes) - bundle/logs/vuln_variant/isFolder_vulnerable.log (log, 294 bytes) - bundle/logs/vuln_variant/isFolder_fixed.log (log, 298 bytes) - bundle/logs/vuln_variant/listFolderPath_vulnerable.log (log, 306 bytes) - bundle/logs/vuln_variant/listFolderPath_fixed.log (log, 310 bytes) - bundle/vuln_variant/artifacts/fake_smbclient.sh (other, 159 bytes) - bundle/vuln_variant/artifacts/harness.php (other, 1649 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00288 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00288/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00288 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev