{"repro_id":"REPRO-2026-00288","version":6,"title":"Horde VFS < 3.0.1 OS command injection via Horde_Vfs_Smb driver","repro_type":"security","status":"published","severity":"high","description":"Horde Virtual File System (VFS) API before 3.0.1 contains an OS command injection vulnerability in the Horde_Vfs_Smb driver. Authenticated users can supply crafted filenames through normal VFS operations such as upload, create folder, rename, delete, or read paths. Those names are interpolated into a shell command that reaches proc_open() and /bin/sh -c before smbclient runs. The vulnerable code path is in lib/Horde/Vfs/Smb.php and the issue is fixed in 3.0.1 by eliminating shell-string execution and hardening SMB argument quoting.","root_cause":"# Root Cause Analysis: CVE-2026-60102\n\n## Summary\n\nHorde 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`.\n\n## Impact\n\n- **Package/component:** Horde VFS, specifically the `Horde_Vfs_Smb` driver (`lib/Horde/Vfs/Smb.php`).\n- **Affected versions:** All versions before commit `41f74b4acfc144e09013d04dd121e0a5da808361` (released as `v3.0.1`).\n- **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.\n- **Consequences:** Full OS command execution as the user running the PHP process.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Code execution (OS command injection) via `proc_open()` / `/bin/sh -c`.\n- **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.\n- **Parity:** `full` for the `createFolder()` path; the same quoting weakness is present in all VFS operations that build the `smbclient -c` command string.\n- **Not demonstrated:** No remote authentication bypass was needed; the reproduction assumes an already authenticated VFS caller as described in the CVE.\n\n## Root Cause\n\nIn the vulnerable code (`lib/Horde/Vfs/Smb.php` before `41f74b4^`), `_command()` concatenates user-supplied paths/names into a single string:\n\n```php\n$fullcmd = $this->_params['smbclient']\n    . ' \"//' . $this->_params['hostspec'] . '/' . $share . '\"'\n    . ' \"-U' . $this->_params['username'] . '\"'\n    . ' -D \"' . $path . '\"'\n    . ' -c \"';\nforeach ($cmd as $c) {\n    $fullcmd .= $c . \";\";\n}\n$fullcmd .= '\"';\nreturn $this->_execute($fullcmd);\n```\n\n`_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`.\n\nThe 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.\n\n## Reproduction Steps\n\n1. Run `bundle/repro/reproduction_steps.sh` from the bundle directory.\n2. The script checks out the vulnerable commit (`41f74b4^`) and the fixed commit (`41f74b4`) of `horde/Vfs` into a cached repository.\n3. It installs PHP and Composer, then runs `composer install` for each commit.\n4. It creates a fake `smbclient` that logs arguments and exits 0.\n5. 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')`.\n6. 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.\n\n## Evidence\n\n- `bundle/logs/reproduction_steps.log` — overall run summary, commit SHAs, and captured fake `smbclient` arguments.\n- `bundle/logs/vulnerable.log` — shows `Marker exists: YES` with `uid=1000(vscode) ...`.\n- `bundle/logs/fixed.log` — shows `Marker exists: NO`.\n- `bundle/logs/vulnerable_smbclient.log` — the fake `smbclient` receives `mkdir \".dir\";` because the shell already executed `$(id > ...)` and substituted an empty string.\n- `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.\n- `bundle/repro/artifacts/fake_smbclient.sh` — the fake `smbclient` used as the command target.\n- `bundle/repro/artifacts/harness.php` — the PHP harness that drives the real library path.\n\nKey excerpts:\n\n```\n=== vulnerable (994f4a46fd76ea44eae2fe839216bb273ce49080) ===\nMarker exists: YES\nMarker content: uid=1000(vscode) gid=1000(vscode) groups=1000(vscode),969(969)\n```\n\n```\n=== fixed (41f74b4acfc144e09013d04dd121e0a5da808361) ===\nMarker exists: NO\nARG //127.0.0.1/share\nARG -c\nARG mkdir \"$(id > /tmp/horde_vfs_smb_poc_folder).dir\";\n```\n\n## Recommendations / Next Steps\n\n- **Upgrade guidance:** Upgrade to Horde VFS `v3.0.1` or any release containing commit `41f74b4acfc144e09013d04dd121e0a5da808361`.\n- **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.\n- **Testing recommendations:** Add unit tests for `createFolder`, `write`, `rename`, `delete`, and `readFile` with payloads containing `$(...)`, backticks, pipes, semicolons, and quotes to prevent regression.\n\n## Additional Notes\n\n- **Idempotency:** The reproduction script was run twice consecutively with identical results; both the vulnerable and fixed commits behaved consistently.\n- **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.\n- **Environment:** Ubuntu 26.04, PHP 8.5, Composer 2.9, `horde/Vfs` repository at `https://github.com/horde/Vfs`.\n","cve_id":"CVE-2026-60102","source_url":"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","package":{"name":"horde/vfs","ecosystem":"github","affected_versions":"< 3.0.1","fixed_version":"3.0.1"},"reproduced_at":"2026-07-14T04:21:11.529217+00:00","duration_secs":770.0,"tool_calls":135,"handoffs":2,"total_cost_usd":1.29792461,"agent_costs":{"hypothesis_generator":0.02880815,"judge":0.22297600000000004,"repro":0.35153829999999997,"support":0.19556246,"vuln_variant":0.49903970000000014},"cost_breakdown":{"hypothesis_generator":{"accounts/fireworks/models/kimi-k2p7-code":0.02880815},"judge":{"gpt-5.5":0.22297600000000004},"repro":{"accounts/fireworks/models/kimi-k2p7-code":0.35153829999999997},"support":{"accounts/fireworks/models/kimi-k2p7-code":0.19556246},"vuln_variant":{"accounts/fireworks/models/kimi-k2p7-code":0.49903970000000014}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-14T04:21:12.419019+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":6840,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":6351,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":6536,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":7740,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":13549,"category":"other"},{"path":"bundle/artifact_promotion_report.json","filename":"artifact_promotion_report.json","size":13567,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":753,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":2236,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":820,"category":"log"},{"path":"bundle/logs/vulnerable.log","filename":"vulnerable.log","size":267,"category":"log"},{"path":"bundle/logs/fixed.log","filename":"fixed.log","size":187,"category":"log"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":713,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":780,"category":"other"},{"path":"bundle/logs/vulnerable_smbclient.log","filename":"vulnerable_smbclient.log","size":144,"category":"log"},{"path":"bundle/logs/fixed_smbclient.log","filename":"fixed_smbclient.log","size":192,"category":"log"},{"path":"bundle/repro/artifacts/fake_smbclient.sh","filename":"fake_smbclient.sh","size":159,"category":"other"},{"path":"bundle/repro/artifacts/harness.php","filename":"harness.php","size":1162,"category":"other"},{"path":"bundle/logs/vuln_variant/variant_run.log","filename":"variant_run.log","size":6062,"category":"log"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":1009,"category":"other"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":2792,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":6033,"category":"documentation"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":1666,"category":"other"},{"path":"bundle/logs/vuln_variant/createFolder_vulnerable.log","filename":"createFolder_vulnerable.log","size":302,"category":"log"},{"path":"bundle/logs/vuln_variant/createFolder_fixed.log","filename":"createFolder_fixed.log","size":306,"category":"log"},{"path":"bundle/logs/vuln_variant/deleteFile_vulnerable.log","filename":"deleteFile_vulnerable.log","size":298,"category":"log"},{"path":"bundle/logs/vuln_variant/deleteFile_fixed.log","filename":"deleteFile_fixed.log","size":302,"category":"log"},{"path":"bundle/logs/vuln_variant/writeData_vulnerable.log","filename":"writeData_vulnerable.log","size":296,"category":"log"},{"path":"bundle/logs/vuln_variant/writeData_fixed.log","filename":"writeData_fixed.log","size":300,"category":"log"},{"path":"bundle/logs/vuln_variant/rename_vulnerable.log","filename":"rename_vulnerable.log","size":290,"category":"log"},{"path":"bundle/logs/vuln_variant/rename_fixed.log","filename":"rename_fixed.log","size":294,"category":"log"},{"path":"bundle/logs/vuln_variant/readFile_vulnerable.log","filename":"readFile_vulnerable.log","size":294,"category":"log"},{"path":"bundle/logs/vuln_variant/readFile_fixed.log","filename":"readFile_fixed.log","size":298,"category":"log"},{"path":"bundle/logs/vuln_variant/deleteFolder_vulnerable.log","filename":"deleteFolder_vulnerable.log","size":302,"category":"log"},{"path":"bundle/logs/vuln_variant/deleteFolder_fixed.log","filename":"deleteFolder_fixed.log","size":306,"category":"log"},{"path":"bundle/logs/vuln_variant/isFolder_vulnerable.log","filename":"isFolder_vulnerable.log","size":294,"category":"log"},{"path":"bundle/logs/vuln_variant/isFolder_fixed.log","filename":"isFolder_fixed.log","size":298,"category":"log"},{"path":"bundle/logs/vuln_variant/listFolderPath_vulnerable.log","filename":"listFolderPath_vulnerable.log","size":306,"category":"log"},{"path":"bundle/logs/vuln_variant/listFolderPath_fixed.log","filename":"listFolderPath_fixed.log","size":310,"category":"log"},{"path":"bundle/vuln_variant/artifacts/fake_smbclient.sh","filename":"fake_smbclient.sh","size":159,"category":"other"},{"path":"bundle/vuln_variant/artifacts/harness.php","filename":"harness.php","size":1649,"category":"other"}]}