{"repro_id":"REPRO-2026-00334","version":7,"title":"Authenticated command injection in pm2panel's /restart handler allows remote shell command execution on the host.","repro_type":"security","status":"published","severity":"high","description":"`4xmen/pm2panel` contains an OS command injection vulnerability in the authenticated `/restart` endpoint. The handler concatenates the attacker-controlled `req.query.id` value directly into a shell command (`exec(\"pm2 restart \" + req.query.id)`), allowing shell metacharacters to be interpreted by `/bin/sh` and resulting in arbitrary command execution on the server.","root_cause":"# CVE-2026-72573: Command Injection in pm2panel /restart Endpoint\n\n## Summary\n\nThe `4xmen/pm2panel` web application contains an OS command injection vulnerability in the authenticated `/restart` HTTP endpoint. The handler at line 188 of `pm2panel.js` concatenates the attacker-controlled `req.query.id` query parameter directly into a `child_process.exec()` shell command (`exec(\"pm2 restart \" + req.query.id)`). Because `exec` invokes `/bin/sh`, shell metacharacters such as `;`, `&&`, `|`, backticks, or `$()` in the `id` parameter are interpreted by the shell, allowing an authenticated attacker to execute arbitrary commands on the host with the privileges of the pm2panel process.\n\n## Impact\n\n- **Package/component affected:** `4xmen/pm2panel` (Node.js web application, `pm2panel.js`)\n- **Affected versions:** All versions (commit `dd2a7d2e8cb4dacefb618ab54b0a8f7dc6742fa0`, latest on `master`)\n- **Risk level:** High — authenticated remote code execution\n- **Consequences:** An attacker with valid panel credentials (default: `admin`/`admin`) can execute arbitrary shell commands on the host server, leading to full system compromise, data exfiltration, lateral movement, or service disruption.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Code execution (CWE-78, OS command injection)\n- **Reproduced impact from this run:** Full arbitrary command execution — a marker file was created on the host filesystem via an injected `touch` command through the authenticated `/restart` endpoint\n- **Parity:** `full`\n- **Not demonstrated:** N/A — the full claimed impact was demonstrated\n\n## Root Cause\n\nThe `/restart` route handler in `pm2panel.js` (line 188) constructs a shell command by directly string-concatenating the user-supplied `req.query.id` query parameter:\n\n```js\napp.get('/restart', function (req, res) {\n    if (!req.session.islogin) {\n        // redirect to login...\n    } else {\n        if (req.query.id) {\n            exec(\"pm2 restart \" + req.query.id, (error, stdout, stderr) => {\n                // ...\n            });\n        }\n    }\n});\n```\n\n`child_process.exec()` spawns a shell (`/bin/sh -c`) to run the command. The `req.query.id` value is never validated, sanitized, or shell-escaped. When an attacker sends a request like `GET /restart?id=0; touch /tmp/pm2panel_pwned`, the shell interprets the `;` as a command separator and executes `touch /tmp/pm2panel_pwned` in addition to `pm2 restart 0`.\n\nThe same vulnerable pattern exists in four other handlers:\n- `/start` (line 218): `exec(\"pm2 start \" + req.query.id)`\n- `/stop` (line 248): `exec(\"pm2 stop \" + req.query.id)`\n- `/delete` (line 278): `exec(\"pm2 delete \" + req.query.id)`\n- `/addProccess` (line 149): `exec('pm2 start \"' + req.body.path + '\"')`\n\nNo fix commit has been identified; the vulnerability is present in the latest commit on `master`.\n\n## Reproduction Steps\n\n1. **Reference script:** `bundle/repro/reproduction_steps.sh`\n2. **What the script does:**\n   - Clones/reuses the `4xmen/pm2panel` repository from the project cache\n   - Installs system dependencies (`libpam0g-dev`) and npm dependencies (including native `node-linux-pam` module)\n   - Installs and starts PM2 with a demo process (id 0)\n   - Starts the pm2panel Express web application on port 3001\n   - Authenticates via `POST /loginCheck` with default credentials (`admin`/`admin`)\n   - Sends an authenticated `GET /restart?id=0; touch /tmp/pm2panel_pwned_<pid>` request\n   - Verifies the marker file was created (proving arbitrary command execution)\n   - Runs negative controls: unauthenticated request is rejected (302 redirect), safe request without injection does not create a marker\n3. **Expected evidence of reproduction:**\n   - Marker file exists at `/tmp/pm2panel_pwned_*` after the exploit request\n   - HTTP 302 response from the exploit endpoint (normal redirect behavior)\n   - Unauthenticated requests return 302 redirect to `/login`\n   - Safe restart requests (no injection) do not create marker files\n\n## Evidence\n\n- **Log files:**\n  - `bundle/logs/reproduction_steps.log` — full script execution log\n  - `bundle/logs/pm2panel_service.log` — pm2panel application server log\n  - `bundle/logs/artifacts/http/response_login.txt` — login response with session cookie\n  - `bundle/logs/artifacts/http/request_exploit.txt` — exploit request details\n  - `bundle/logs/artifacts/http/response_exploit.txt` — exploit HTTP response (302)\n  - `bundle/logs/artifacts/http/marker_evidence.txt` — marker file existence and stat output\n  - `bundle/logs/artifacts/http/response_unauth.txt` — unauthenticated request response (302 redirect)\n  - `bundle/logs/artifacts/http/safe_restart_status.txt` — safe restart response code\n  - `bundle/repro/runtime_manifest.json` — structured runtime evidence manifest\n\n- **Key excerpts:**\n  - Exploit request: `GET /restart?id=0;%20touch%20/tmp/pm2panel_pwned_3850 HTTP/1.1`\n  - Exploit response: `HTTP/1.1 302 Found` with `Location: /`\n  - Marker evidence: `MARKER_FILE_EXISTS=true` with `stat` output showing file creation timestamp\n  - Negative control (unauthenticated): `302` redirect to `/login`\n  - Negative control (safe): no marker file created\n\n- **Environment:**\n  - Node.js v24.18.0, npm 11.16.0\n  - PM2 v7.0.3\n  - pm2panel commit `dd2a7d2e8cb4dacefb618ab54b0a8f7dc6742fa0`\n  - Linux x86_64, Express 4.x, express-session\n\n## Recommendations / Next Steps\n\n1. **Fix:** Replace `child_process.exec` with `child_process.execFile` (which does not invoke a shell) and pass `req.query.id` as a separate argument array, or validate `req.query.id` against a strict numeric regex before use.\n2. **Defense in depth:** Implement input validation on all endpoints that accept process IDs (`/start`, `/stop`, `/delete`, `/addProccess`).\n3. **Authentication:** Change default credentials from `admin`/`admin` and enforce strong password policies.\n4. **Upgrade guidance:** No patched version exists. Users should apply the fix manually or discontinue use of the panel.\n5. **Testing:** Add integration tests that send shell metacharacters in query parameters and assert they are not interpreted by the shell.\n\n## Additional Notes\n\n- **Idempotency:** The script uses process-specific marker file names (with `$$` PID suffix) and cleans up PM2 processes and the pm2panel server on each run. It was verified to pass on two consecutive executions.\n- **Authentication requirement:** The vulnerability requires authentication. The script performs a proper login flow with `POST /loginCheck` and session cookie extraction before sending the exploit request.\n- **Multiple vulnerable endpoints:** The same command injection pattern affects `/start`, `/stop`, `/delete`, and `/addProccess` in addition to `/restart`. The reproduction focuses on `/restart` as specified in the claim.\n","cve_id":"CVE-2026-72573","cwe_id":"CWE-78 (OS Command Injection)","source_url":"https://nvd.nist.gov/vuln/detail/CVE-2026-72573","package":{"name":"4xmen/pm2panel","ecosystem":"GitHub / Node.js web application","affected_versions":"All versions (version 0 affected per CVE record, defaultStatus unknown)","fixed_version":"None identified - no fix available"},"reproduced_at":"2026-08-23T15:44:16.384819+00:00","duration_secs":854.0,"tool_calls":174,"handoffs":2,"total_cost_usd":1.483165,"agent_costs":{"claim_matcher":0.019465,"judge":0.085533,"learning_policy":0.015313,"repro":0.944135,"support":0.037662,"vuln_variant":0.381057},"cost_breakdown":{"claim_matcher":{"gpt-5.4-mini-2026-03-17":0.019465},"judge":{"gpt-5.4-mini":0.052794,"gpt-5.4-mini-2026-03-17":0.032739},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.015313},"repro":{"accounts/fireworks/routers/glm-5p2-fast":0.944135},"support":{"accounts/fireworks/routers/glm-5p2-fast":0.037662},"vuln_variant":{"accounts/fireworks/routers/glm-5p2-fast":0.381057}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"known_vulnerability","schema_version":2,"stages":["support","claim_contract","repro","judge","vuln_variant"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-08-23T15:44:16.942306+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":6794,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":13715,"category":"reproduction_script"},{"path":"bundle/logs/artifacts/http/negative_control.txt","filename":"negative_control.txt","size":282,"category":"other"},{"path":"bundle/logs/artifacts/http/pm2panel_service_final.log","filename":"pm2panel_service_final.log","size":455,"category":"log"},{"path":"bundle/logs/artifacts/http/response_login.txt","filename":"response_login.txt","size":8887,"category":"other"},{"path":"bundle/logs/artifacts/http/safe_restart_status.txt","filename":"safe_restart_status.txt","size":3,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1214,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":859,"category":"other"}]}