T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/export-pdf.sh:165
- Finding
- Local HTTP Server Permits Directory Traversal and Unnecessarily Listens Beyond Loopback<![CDATA[ ## Vulnerability Details **File Location**: `scripts/export-pdf.sh`, lines 165-181 **Vulnerability Type**: Directory traversal and excessive network exposure **Risk Level**: High ### Vulnerable Code ```javascript const server = createServer((req, res) => { // Decode URL-encoded characters (e.g., %20 → space) so filenames with spaces resolve correctly const decodedUrl = decodeURIComponent(req.url); let filePath = join(SERVE_DIR, decodedUrl === '/' ? HTML_FILE : decodedUrl); try { const content = readFileSync(filePath); const ext = extname(filePath).toLowerCase(); res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' }); res.end(content); } catch { res.writeHead(404); res.end('Not found'); } }); // Find a free port const port = await new Promise((resolve) => { server.listen(0, () => resolve(server.address().port)); }); ``` ### Technical Analysis The temporary static-file server treats the request URL as a filesystem path after applying `decodeURIComponent()`. It then combines that value with `SERVE_DIR` using `join()` and reads the result without verifying that the normalized path remains inside the intended presentation directory. A path containing parent-directory components can therefore resolve outside `SERVE_DIR`. Encoded traversal components are particularly relevant because URL decoding occurs before filesystem resolution. The server also calls `server.listen(0)` without specifying a loopback address. Depending on the host configuration and Node.js behavior, this can bind to an unspecified address rather than strictly to `127.0.0.1`. Although the port is dynamically selected, relying on port obscurity is not an access-control mechanism. ### Attack Path 1. A user invokes `scripts/export-pdf.sh` to export a presentation. 2. The script starts the generated Node.js HTTP server. 3. The server listens on a dynamically selected port without an explicit loopback restriction. 4. A ...[truncated 1004 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Bind the server explicitly to the loopback interface: ```javascript server.listen(0, '127.0.0.1', () => { resolve(server.address().port); }); ``` 2. Parse only the pathname component rather than using the complete raw request URL. 3. Resolve the requested path against a canonical root and verify containment before reading it: ```javascript import { resolve, sep, extname } from 'path'; const root = resolve(SERVE_DIR); const server = createServer((req, res) => { try { const pathname = decodeURIComponent( new URL(req.url, 'http://127.0.0.1').pathname ); const relativePath = pathname === '/' ? HTML_FILE : `.${pathname}`; const filePath = resolve(root, relativePath); if (filePath !== root && !filePath.startsWith(root + sep)) { res.writeHead(403); res.end('Forbidden'); return; } const content = readFileSync(filePath); const ext = extname(filePath).toLowerCase(); res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream', 'X-Content-Type-Options': 'nosniff' }); res.end(content); } catch { res.writeHead(404); res.end('Not found'); } }); ``` 4. Reject malformed encoding, null bytes, and paths containing unsupported path syntax. 5. Consider serving only an explicit allowlist of files discovered from the presentation rather than exposing the entire parent directory. 6. Add automated tests for plain, encoded, and double-encoded traversal attempts. ]]>
