T09 · Insecure Skill Coding Practices
- Location
references/nodejs.md:62- Finding
Arbitrary Local File Disclosure in the Inline Node.js Server
- Content
View full analysis
{require('fs').createReadStream('.'+req.url).pipe(res)}).listen(8000)" ``` The accompanying note only states that the implementation does not handle errors, MIME types, or directory listings. It does not warn that request paths are used unsafely. ### Technical Analysis The server concatenates the attacker-controlled `req.url` value directly with `"."` and passes the result to `fs.createReadStream`. It does not: - Parse and decode the URL safely. - Normalize the resulting filesystem path. - Verify that the resolved path remains inside the intended document root. - Reject traversal components. - Prevent symbolic-link escapes. - Restrict the listener to the loopback interface. A path such as `./../../etc/passwd` is resolved by the operating system outside the current working directory. In addition, `listen(8000)` listens on the unspecified address, which generally exposes the server on all available interfaces. ### Attack Path 1. A user starts the documented inline Node.js server in a directory containing files intended for local preview. 2. The process opens port 8000 without explicitly restricting it to loopback. 3. An attacker with network access sends a path-preserving HTTP request containing traversal components, for example: ```bash curl --path-as-is http://target-host:8000/../../etc/passwd ``` 4. The server constructs the filesystem path by evaluating `"." + req.url`. 5. `fs.createReadStream` follows the traversal outside the intended document root. 6. The selected file is streamed to the attacker if it is readable by the Node.js process. ### Impact Assessment An attacker may read files accessible to the account running Node.js. Dependin ...[truncated 502 chars]- Remediation
View remediation
{ try { const pathname = decodeURIComponent(new URL(req.url, "http://localhost").pathname); const candidate = path.resolve(root, "." + pathname); if (candidate !== root && !candidate.startsWith(root + path.sep)) { res.writeHead(403); return res.end("Forbidden"); } const canonical = fs.realpathSync(candidate); if (canonical !== root && !canonical.startsWith(root + path.sep)) { res.writeHead(403); return res.end("Forbidden"); } fs.createReadStream(canonical) .on("error", () => { res.writeHead(404); res.end("Not found"); }) .pipe(res); } catch { res.writeHead(400); res.end("Bad request"); } }).listen(8000, "127.0.0.1"); ``` ]]>
