Path traversal (CWE-22, OWASP A01 Broken Access Control) uses ../ sequences to escape an intended directory, allowing an attacker to read files like /etc/passwd, process.env, or private keys from arbitrary locations on the filesystem. NIST 800-53 SI-10 requires input sanitization before filesystem operations. Any endpoint that accepts a filename parameter — downloads, file preview, static serving — is a potential traversal vector if the path is not normalized and boundary-checked. A URL like /api/download?file=../../.env against a naive path.join implementation returns the application's secrets.
Low because path traversal requires a file-serving endpoint with user-controlled path input, but exploitation yields direct filesystem read access to arbitrary files.
Normalize every user-supplied path with path.resolve() and verify it starts with the expected base directory:
import path from 'path'
const UPLOADS_DIR = path.resolve(process.cwd(), 'uploads')
function safeReadFile(userFilename: string): Buffer {
const resolved = path.resolve(UPLOADS_DIR, userFilename)
if (!resolved.startsWith(UPLOADS_DIR + path.sep)) {
throw new Error('Access denied')
}
return require('fs').readFileSync(resolved)
}
Store uploaded files with random UUIDs as names (crypto.randomUUID()) rather than user-supplied filenames to eliminate traversal entirely. Serve files from external blob storage (Vercel Blob, S3) where path traversal is not applicable.
ID: security-hardening.secrets-config.path-traversal
Severity: low
What to look for: Enumerate every file system access that uses user-supplied input (file reads, downloads, uploads, static serving). For each, search for filesystem operations that include user-controlled values in paths: fs.readFile, fs.writeFile, path.join, path.resolve with user input. Check endpoints that serve files by name or download endpoints that accept a filename parameter. Look for ../ sequences or equivalent traversal in path construction.
Pass criteria: Any path constructed from user input is normalized and validated to remain within an expected base directory. path.resolve() is used and the result is verified to start with the expected directory prefix — 100% of path operations must normalize and validate against directory traversal. Report: "X file access points with user input found, all Y sanitize paths."
Fail criteria: User-controlled filenames or paths are used directly in filesystem operations without sanitization or boundary checking.
Skip (N/A) when: The application performs no filesystem operations based on user input.
Detail on fail: "GET /api/download?file= parameter used directly in fs.readFile without path normalization — directory traversal possible" or "File serving endpoint constructs path with user-supplied filename without checking it stays within uploads directory"
Remediation: Normalize and validate all file paths:
import path from 'path'
const UPLOADS_DIR = path.resolve(process.cwd(), 'uploads')
function safeReadFile(userFilename: string): string {
// Normalize the path (resolves .., removes duplicates)
const resolved = path.resolve(UPLOADS_DIR, userFilename)
// Verify the resolved path is within the expected directory
if (!resolved.startsWith(UPLOADS_DIR + path.sep)) {
throw new Error('Access denied: path traversal attempt detected')
}
return require('fs').readFileSync(resolved, 'utf8')
}