T09 · Insecure Skill Coding Practices
- Location
scripts/export-pdf.sh:159- Finding
Directory Traversal in Network-Accessible PDF Export Server
- Content
View full analysis
{ // 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 PDF exporter creates a temporary HTTP server to serve the directory containing the input presentation. The request URL is decoded and passed directly to `join(SERVE_DIR, ...)`, after which the resulting path is read with `readFileSync`. The implementation does not canonicalize the requested path and verify that it remains inside `SERVE_DIR`. Consequently, traversal components in an HTTP request can resolve to files outside the presentation directory. The server also uses `server.listen(0)` without explicitly binding to a loopback address. Depending on the Node.js runtime and host configuration, this can listen on unspecified network interfaces rather than only `127.0.0.1`. This expands the attack surface from local processes to network-adjacent clients that can discover or otherwise access the temporary port. Additionally, `decodeURIComponent(req.url)` is outside the `try` block. Invalid percent-encoding can throw an exception and terminate the exporter, creating a denial-of-service condition. ### Attack P ...[truncated 1398 chars]- Remediation
View remediation
{ resolve(server.address().port); }); ``` 2. Parse only the URL pathname, decode it inside a protected error handler, and reject malformed requests. 3. Canonicalize both the serving root and requested path with `resolve()`. 4. Verify that the canonical requested path equals the root or begins with the root followed by the platform path separator. 5. Reject NUL bytes, parent-directory traversal, and paths outside the serving root with HTTP 403. 6. Import and use `sep` from Node.js `path` to avoid unsafe string-prefix comparisons. Example hardened implementation: ```javascript import { resolve, sep, extname } from 'path'; const root = resolve(SERVE_DIR); const server = createServer((req, res) => { try { const pathname = new URL(req.url, 'http://127.0.0.1').pathname; const decodedPath = decodeURIComponent(pathname); if (decodedPath.includes('\0')) { res.writeHead(400); return res.end('Bad request'); } const requestedPath = decodedPath === '/' ? resolve(root, HTML_FILE) : resolve(root, `.${decodedPath}`); const insideRoot = requestedPath === root || requestedPath.startsWith(root + sep); if (!insideRoot) { res.writeHead(403); return res.end('Forbidden'); } const content = readFileSync(requestedPath); const ext = extname(requestedPath).toLowerCase(); res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' }); res.end(content); } catch { res.writeHead(404); res.end('Not found'); } }); server.listen(0, '127.0.0.1', () => { console.log(`Local server on port ${server.address().port}`); }); ``` 7. Add automated tests for plain and encoded traversal attempts, malformed percent-encoding, absolute paths, and valid files below the serving root. ]]>
