{"repro_id":"REPRO-2026-00140","version":8,"title":"zenshin: OS command injection in /stream-to-vlc url query parameter","repro_type":"security","status":"published","severity":"critical","description":"`zenshin` is an Electron application with an Express HTTP backend. The backend\nroute **`/stream-to-vlc`** passes the `url` query parameter into a **shell\ncommand without sanitization**. A crafted `url` value containing shell\nmetacharacters injects and executes **arbitrary shell commands** on the host —\nremote code execution.\n\nAny actor able to reach the local Express backend port can issue the request,\n**unauthenticated**.","root_cause":"# RCA Report — CVE-2026-37281\n\n## Summary\n\n`zenshin` is an Electron application with an embedded Express HTTP backend. The backend route `GET /stream-to-vlc` takes a `url` query parameter and interpolates it unsanitized into a shell command executed via `child_process.exec`. A crafted `url` value containing shell metacharacters (e.g., a closing quote followed by a command separator) allows arbitrary command execution on the host, resulting in unauthenticated remote code execution for any actor that can reach the backend port.\n\n## Impact\n\n- **Package/component affected**: `zenshin` (Electron/Express backend)\n- **Affected versions**: `< 2.7.0`\n- **Fixed version**: `2.7.0` (commit `7d31c6edfbac978f0ad44c66d761bab9dcd2fa27`)\n- **Risk level**: Critical — CVSS 3.1 base 9.8\n- **Consequences**: Unauthenticated attackers on the local network (or any host that can reach the Express port) can execute arbitrary OS commands as the user running zenshin.\n\n## Root Cause\n\nIn the vulnerable commit (`7d31c6edfbac978f0ad44c66d761bab9dcd2fa27~1`), the `/stream-to-vlc` route in `Electron/zenshin-electron/src/main/index.js` constructs a shell command by directly interpolating the user-supplied `url` query parameter:\n\n```javascript\nconst vlcPath = '\"C:\\\\Program Files (x86)\\\\VideoLAN\\\\VLC\\\\vlc.exe\"'\nconst vlcCommand = `${vlcPath} \"${url}\"`\nexec(vlcCommand, (error) => { ... })\n```\n\nBecause `exec` invokes `/bin/sh -c` (or `cmd.exe /c` on Windows), any shell metacharacters in `url` are interpreted by the shell. An attacker can break out of the double-quote wrapper with a payload such as `x\"; touch /tmp/pwned; echo \"x`, causing the shell to execute the injected `touch /tmp/pwned` command after the (non-existent) VLC executable fails.\n\nThe fix commit (`7d31c6edfbac978f0ad44c66d761bab9dcd2fa27`) completely removes the `/stream-to-vlc` Express endpoint and replaces the `ipcMain` handler with `spawn`, passing the player path and URL as separate array arguments with `shell: false`, which prevents shell interpretation entirely.\n\n## Reproduction Steps\n\nThe reproduction script is `repro/reproduction_steps.sh`. It performs the following steps:\n\n1. Checks out the vulnerable commit (`7d31c6edfbac978f0ad44c66d761bab9dcd2fa27~1`).\n2. Installs dependencies and builds the project with `npm run build` inside `Electron/zenshin-electron`.\n3. Runs the actual built `out/main/index.js` with a Node.js mock harness that stubs `electron`, `@electron-toolkit/utils`, and `electron-deeplink`, allowing the Express backend to start without a real Electron runtime.\n4. Sends a crafted `GET /stream-to-vlc?url=...` request with the payload `x\"; touch /tmp/pwned; echo \"x`.\n5. Verifies that `/tmp/pwned` is created, proving arbitrary command execution.\n6. Repeats steps 1–5 with the fixed commit (`7d31c6edfbac978f0ad44c66d761bab9dcd2fa27`).\n7. Verifies that the endpoint returns `404 Cannot GET /stream-to-vlc` and `/tmp/pwned` is NOT created.\n\n## Evidence\n\n- **Vulnerable commit result**: `statusCode=200`, `responseBody=\"VLC launched successfully\"`, `pwned_exists=true`\n- **Fixed commit result**: `statusCode=404`, `responseBody=\"Cannot GET /stream-to-vlc\"`, `pwned_exists=false`\n- **Log files**:\n  - `logs/vulnerable.json` — HTTP response and sentinel file state for the vulnerable build\n  - `logs/fixed.json` — HTTP response and sentinel file state for the fixed build\n  - `logs/run.log` — full build and execution logs\n- **Runtime manifest**: `repro/runtime_manifest.json` captures the structured verdict.\n\n## Recommendations / Next Steps\n\n- **Fix approach**: Remove or replace `exec` with `spawn` using an array of arguments and `shell: false`, as the upstream fix does. This prevents the shell from parsing user input.\n- **Input validation**: If the endpoint must be retained, strictly validate the `url` parameter against an allow-list of safe stream URL patterns before passing it to any process launcher.\n- **Network binding**: Bind the Express backend to `127.0.0.1` only (also done in the fix) to reduce the attack surface.\n- **Testing**: Add integration tests that attempt shell injection against `/stream-to-vlc` and similar endpoints to prevent regressions.\n\n## Additional Notes\n\n- **Idempotency**: The reproduction script was run twice consecutively with identical results on both runs.\n- **Edge cases / limitations**: The reproduction uses a mock Electron environment because a real Electron GUI cannot run in a headless CI/container environment. The mock stubs only Electron-specific APIs (window creation, IPC, deeplinks); the Express backend and the vulnerable route handler are the actual transpiled code from the project build.\n","cve_id":"CVE-2026-37281","cwe_id":"CWE-78 (OS Command Injection)","reproduced_at":"2026-05-22T10:22:33.455396+00:00","duration_secs":1683.2075805664062,"tool_calls":190,"turns":146,"handoffs":2,"total_cost_usd":1.3671912699999995,"agent_costs":{"repro":0.6409107599999999,"support":0.021384080000000003,"vuln_variant":0.7048964300000002},"cost_breakdown":{"repro":{"accounts/fireworks/models/kimi-k2p6":0.6409107599999999},"support":{"accounts/fireworks/models/kimi-k2p6":0.021384080000000003},"vuln_variant":{"accounts/fireworks/models/kimi-k2p6":0.7048964300000002}},"quality":{"idempotent_verified":false,"community_verifications":0},"published_at":"2026-05-22T10:22:35.587734+00:00","retracted":false,"artifacts":[{"path":"repro/rca_report.md","filename":"rca_report.md","size":4648,"category":"analysis"},{"path":"repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":6613,"category":"reproduction_script"},{"path":"vuln_variant/rca_report.md","filename":"rca_report.md","size":7043,"category":"analysis"},{"path":"vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":11144,"category":"reproduction_script"},{"path":"bundle/context.json","filename":"context.json","size":2658,"category":"other"},{"path":"bundle/metadata.json","filename":"metadata.json","size":643,"category":"other"},{"path":"bundle/ticket.md","filename":"ticket.md","size":3220,"category":"ticket"},{"path":"repro/runtime_manifest.json","filename":"runtime_manifest.json","size":614,"category":"other"},{"path":"repro/validation_verdict.json","filename":"validation_verdict.json","size":1819,"category":"other"},{"path":"vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1136,"category":"other"},{"path":"vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":5200,"category":"documentation"},{"path":"vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":3089,"category":"other"},{"path":"vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":2132,"category":"other"},{"path":"vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":2287,"category":"other"},{"path":"logs/fixed.json","filename":"fixed.json","size":256,"category":"other"},{"path":"logs/fixed-npm-build.log","filename":"fixed-npm-build.log","size":14258,"category":"log"},{"path":"logs/worktree-fix.log","filename":"worktree-fix.log","size":134,"category":"log"},{"path":"logs/fixed-runner.log","filename":"fixed-runner.log","size":376,"category":"log"},{"path":"logs/vulnerable.json","filename":"vulnerable.json","size":119,"category":"other"},{"path":"logs/fix_static_analysis.log","filename":"fix_static_analysis.log","size":245,"category":"log"},{"path":"logs/fix_original-npm-install.log","filename":"fix_original-npm-install.log","size":1063,"category":"log"},{"path":"logs/vuln_original.json","filename":"vuln_original.json","size":186,"category":"other"},{"path":"logs/vuln_alt.json","filename":"vuln_alt.json","size":174,"category":"other"},{"path":"logs/fix_original-runner.log","filename":"fix_original-runner.log","size":387,"category":"log"},{"path":"logs/fix_alt-runner.log","filename":"fix_alt-runner.log","size":380,"category":"log"},{"path":"logs/spawn-injection-runner.log","filename":"spawn-injection-runner.log","size":0,"category":"log"},{"path":"logs/worktree-vuln.log","filename":"worktree-vuln.log","size":109,"category":"log"},{"path":"logs/mock-runner-variant.js","filename":"mock-runner-variant.js","size":3574,"category":"other"},{"path":"logs/vuln_alt-runner.log","filename":"vuln_alt-runner.log","size":562,"category":"log"},{"path":"logs/variant_attempt_summary.json","filename":"variant_attempt_summary.json","size":356,"category":"other"},{"path":"logs/run.log","filename":"run.log","size":63218,"category":"log"},{"path":"logs/fix_alt.json","filename":"fix_alt.json","size":322,"category":"other"},{"path":"logs/fixed-npm-install.log","filename":"fixed-npm-install.log","size":1054,"category":"log"},{"path":"logs/vuln_alt-npm-build.log","filename":"vuln_alt-npm-build.log","size":14277,"category":"log"},{"path":"logs/fix_alt-npm-build.log","filename":"fix_alt-npm-build.log","size":14277,"category":"log"},{"path":"logs/vulnerable-npm-build.log","filename":"vulnerable-npm-build.log","size":14258,"category":"log"},{"path":"logs/mock-runner.js","filename":"mock-runner.js","size":3266,"category":"other"},{"path":"logs/fix_alt-npm-install.log","filename":"fix_alt-npm-install.log","size":1063,"category":"log"},{"path":"logs/vuln_original-npm-install.log","filename":"vuln_original-npm-install.log","size":1064,"category":"log"},{"path":"logs/spawn_injection_test.log","filename":"spawn_injection_test.log","size":47,"category":"log"},{"path":"logs/fix_original-npm-build.log","filename":"fix_original-npm-build.log","size":14276,"category":"log"},{"path":"logs/vulnerable-runner.log","filename":"vulnerable-runner.log","size":380,"category":"log"},{"path":"logs/vuln_alt-npm-install.log","filename":"vuln_alt-npm-install.log","size":1064,"category":"log"},{"path":"logs/stale_vuln_scan.log","filename":"stale_vuln_scan.log","size":359,"category":"log"},{"path":"logs/vuln_original-npm-build.log","filename":"vuln_original-npm-build.log","size":14277,"category":"log"},{"path":"logs/vulnerable-npm-install.log","filename":"vulnerable-npm-install.log","size":1054,"category":"log"},{"path":"logs/fix_original.json","filename":"fix_original.json","size":327,"category":"other"},{"path":"logs/vuln_original-runner.log","filename":"vuln_original-runner.log","size":387,"category":"log"}]}