What's the vulnerability?

zenshin is an Electron application with an Express HTTP backend. The backend route /stream-to-vlc passes the url query parameter into a shell command without sanitization. A crafted url value containing shell metacharacters injects and executes arbitrary shell commands on the host — remote code execution.

Any actor able to reach the local Express backend port can issue the request, unauthenticated.

Root Cause Analysis

# RCA Report — CVE-2026-37281

## Summary

`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.

## Impact

- **Package/component affected**: `zenshin` (Electron/Express backend)
- **Affected versions**: `< 2.7.0`
- **Fixed version**: `2.7.0` (commit `7d31c6edfbac978f0ad44c66d761bab9dcd2fa27`)
- **Risk level**: Critical — CVSS 3.1 base 9.8
- **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.

## Root Cause

In 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:

```javascript
const vlcPath = '"C:\\Program Files (x86)\\VideoLAN\\VLC\\vlc.exe"'
const vlcCommand = `${vlcPath} "${url}"`
exec(vlcCommand, (error) => { ... })
```

Because `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.

The 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.

## Reproduction Steps

The reproduction script is `repro/reproduction_steps.sh`. It performs the following steps:

1. Checks out the vulnerable commit (`7d31c6edfbac978f0ad44c66d761bab9dcd2fa27~1`).
2. Installs dependencies and builds the project with `npm run build` inside `Electron/zenshin-electron`.
3. 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.
4. Sends a crafted `GET /stream-to-vlc?url=...` request with the payload `x"; touch /tmp/pwned; echo "x`.
5. Verifies that `/tmp/pwned` is created, proving arbitrary command execution.
6. Repeats steps 1–5 with the fixed commit (`7d31c6edfbac978f0ad44c66d761bab9dcd2fa27`).
7. Verifies that the endpoint returns `404 Cannot GET /stream-to-vlc` and `/tmp/pwned` is NOT created.

## Evidence

- **Vulnerable commit result**: `statusCode=200`, `responseBody="VLC launched successfully"`, `pwned_exists=true`
- **Fixed commit result**: `statusCode=404`, `responseBody="Cannot GET /stream-to-vlc"`, `pwned_exists=false`
- **Log files**:
  - `logs/vulnerable.json` — HTTP response and sentinel file state for the vulnerable build
  - `logs/fixed.json` — HTTP response and sentinel file state for the fixed build
  - `logs/run.log` — full build and execution logs
- **Runtime manifest**: `repro/runtime_manifest.json` captures the structured verdict.

## Recommendations / Next Steps

- **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.
- **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.
- **Network binding**: Bind the Express backend to `127.0.0.1` only (also done in the fix) to reduce the attack surface.
- **Testing**: Add integration tests that attempt shell injection against `/stream-to-vlc` and similar endpoints to prevent regressions.

## Additional Notes

- **Idempotency**: The reproduction script was run twice consecutively with identical results on both runs.
- **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.
One Command

Verify with pruva-verify

Run the Pruva CLI to automatically fetch and execute the reproduction script.

pruva-verify REPRO-2026-00140
or pruva-verify CVE-2026-37281
Install: curl -fsSL https://pruva.dev/install.sh | sh

Or Run Manually

1

Download the script

curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00140/artifacts/reproduction_steps.sh
2

Make executable

chmod +x reproduction_steps.sh
3

Run the script

./reproduction_steps.sh
Run in a VM, container, or disposable environment. This exploits a real vulnerability.

How Pruva Reproduced This

Watch the AI agent's step-by-step process.

Loading session...

Artifacts

No artifacts available