Path Traversal
Path traversal (directory traversal) attacks use sequences like ../../../etc/passwd in user-controlled file paths to access files outside the intended directory. Node.js's fs module will happily follow these paths.
Path traversal = someone asking for 'Room 3, go back, back, back, into the vault.' The ../../../ is climbing stairs backwards out of the allowed floor. The fix: resolve the full address, then check 'are you still in the building?' (startsWith). path.resolve is just GPS—it tells you WHERE you'd end up, not whether you're ALLOWED there.
Vulnerable code: `fs.readFile('./uploads/' + req.params.filename, ...)`. An attacker requests /files/../../../etc/passwd. Fix: use path.resolve() to get the absolute path, then verify it starts with the allowed base directory: `const safe = path.resolve('/uploads', filename); if (!safe.startsWith('/uploads/')) return 403;`
path.join() does not protect against traversal—it resolves ../ sequences but doesn't validate against a base directory. path.resolve() resolves to an absolute path, and combining it with a startsWith check is the correct pattern. Additional considerations: (1) URL encoding (%2F, %2e%2e) can bypass naive string checks—always decode before checking or use path.resolve. (2) Null byte injection (filename%00.jpg) can truncate filenames in some native code. (3) Case sensitivity: Windows paths are case-insensitive while Linux paths are not—check for both if deploying to mixed environments. (4) Use a library like resolve-path or serve-static (which handles this internally) rather than rolling your own.
Path traversal allows file system access outside intended directories via ../ sequences. The fix: resolve the user-provided path with path.resolve('/safe/base', userInput), then assert the result starts with '/safe/base/'. Never use path.join() alone as it resolves ../ but doesn't enforce a base constraint. URL-decode inputs before validation to prevent encoding bypass.
path.resolve('/base', '../etc/passwd') returns '/etc/passwd'—it resolves to an absolute path correctly, but a naive check for '/base' prefix passes because '/etc/passwd'.startsWith('/base') is false, which is actually correct behavior. The trap is thinking path.resolve prevents traversal rather than understanding it only resolves the path; the startsWith check is the actual security control.