The bookDir() function in a Node.js library management application reached production with a critical flaw: it trusted user-created content to name directories on the filesystem. By embedding path traversal sequences in book titles, an attacker could escape the intended library directory and read arbitrary files—including sensitive configuration, credentials, or system data.
Affected Versions
| Affected | not applicable (first-party code) |
| Fixed in | commit-based fix |
| Ecosystem | Node.js |
| CVE / GHSA | not assigned |
| CWE | CWE-22 (Path Traversal) |
The Vulnerability Explained
The vulnerable code accepted a bookId parameter and concatenated it directly with LIBRARY_DIR:
function bookDir(bookId) {
return path.join(LIBRARY_DIR, bookId);
}
The bookId value originated from user-created book titles without any validation. While the codebase did validate filenames for cover art uploads, this validation never extended to the bookId itself. An attacker could create a book titled ../../../etc/passwd, which would generate a bookId containing that exact string.
When path.join(LIBRARY_DIR, '../../../etc/passwd') executed, Node.js's path resolution would traverse upward from LIBRARY_DIR three directory levels, then into /etc/passwd. The application likely used this path to load metadata JSON files, meaning the attacker could read any file with .json extension—or, depending on subsequent code, potentially any file at all by manipulating the extension.
The real-world impact is severe for multi-user library services: any user with book creation privileges could exfiltrate sensitive files from the server, including environment files, service credentials, or application source code.
The Fix
The fix introduces two defensive layers using path.basename():
function bookDir(bookId) {
const safeId = path.basename(String(bookId));
if (!safeId || safeId !== bookId) throw new Error('Invalid bookId');
return path.join(LIBRARY_DIR, safeId);
}
First layer: path.basename(String(bookId)) extracts only the final filename component, stripping any directory traversal sequences. For input ../../../etc/passwd, it returns passwd.
Second layer: The equality check safeId !== bookId validates that no transformation occurred. If bookId contained any path separators, safeId will differ and the function throws an error.
This approach is superior to regex-based filtering because it handles platform-specific separators automatically and cannot be bypassed through encoding tricks—the path module handles normalization internally.
Key Takeaways
-
User-created content needs the same sanitization as explicit file uploads—the fact that
bookIdwas generated from titles rather than direct filename input created a blind spot in the security model. -
path.basename()followed by strict equality validation provides a robust pattern for path sanitization that rejects both obvious traversal (../) and subtle attempts (extra separators, encoded characters). -
Input validation at the trust boundary—the moment user data enters the system—prevents vulnerable values from propagating through the codebase where they might reach multiple sinks.
-
Platform-agnostic path handling requires using the standard library; manual string operations on paths fail across Windows (
\) and Unix (/) separators.
How Orbis AppSec Detected This
- Source: The
bookIdparameter passed tobookDir(), derived from user-created book titles - Sink:
path.join(LIBRARY_DIR, bookId)constructing filesystem paths for JSON file access - Missing control: No validation that
bookIdrepresents a single filename component withinLIBRARY_DIR - CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
- Fix: Apply
path.basename()to extract the safe filename and validate it matches the original input before path construction
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
This vulnerability demonstrates how path traversal can hide in unexpected places—not just explicit file uploads, but any user-influenced string that reaches the filesystem. The bookId parameter's journey from book title to directory path created an exploitable gap that path.basename() validation now closes. Developers should audit any code that transforms user input into filesystem locations, ensuring the transformation includes strict validation at the earliest possible point.