Back to Blog
high SEVERITY3 min read

bookDir() Path Traversal via Unsanitized bookId Parameter

The `bookDir()` function accepted unsanitized `bookId` values derived from user-created book titles, enabling path traversal attacks through `../` sequences. A fix was applied that validates the identifier using `path.basename()` and throws on mismatch, ensuring all resolved paths remain within `LIBRARY_DIR`.

O
By Orbis AppSec
•Published September 28, 2026•Reviewed September 28, 2026

Answer Summary

The `bookDir()` function in main.js accepts a `bookId` parameter that is generated from user-created book titles without validation. An attacker who controls a book title can embed path traversal sequences like `../../../etc/passwd` to escape `LIBRARY_DIR` and read arbitrary JSON files on the filesystem. The fix applies `path.basename()` to strip directory components and validates that the sanitized value matches the original input, rejecting any traversal attempts. This vulnerability is classified as CWE-22 (Path Traversal).

Vulnerability at a Glance

cweCWE-22
fixpath.basename() validation with strict equality check before path construction
riskArbitrary file read via directory escape from user-controlled book titles
languageJavaScript (Node.js)
root causeDirect string concatenation of unsanitized user input into file paths
vulnerabilityPath Traversal

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 bookId was 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 bookId parameter passed to bookDir(), derived from user-created book titles
  • Sink: path.join(LIBRARY_DIR, bookId) constructing filesystem paths for JSON file access
  • Missing control: No validation that bookId represents a single filename component within LIBRARY_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.

Prevention and further reading

Frequently Asked Questions

Why does the fix compare `safeId !== bookId` instead of just checking for `..` characters?

The strict equality check `safeId !== bookId` catches any case where `path.basename()` modified the input—including directory separators, null bytes, or other path manipulation—making it more robust than a simple substring search.

Can an attacker still exploit this if they create a book titled exactly `etc/passwd` without leading dots?

No. `path.basename('etc/passwd')` returns `'passwd'`, which does not equal `'etc/passwd'`, so the validation throws an error. The fix ensures only single-segment, filename-only identifiers pass through.

What happens to legitimate book titles that contain spaces or Unicode characters?

The fix preserves all characters except path separators. Titles like `"My Book Title"` or `"日本語"` remain valid as long as they don't contain `/` or `\` characters that would cause `path.basename()` to truncate them.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #83

Related Articles

high

Express `app.get('*')` Wildcard Handler Path Traversal in watch.js

A first-party Express server's wildcard route handler used `req.url.indexOf('font.woff2')` to gate access to a font file, allowing attackers to bypass the substring check with crafted paths. The fix replaces the catch-all handler with explicit route registration.

high

updateCardBg() Follows Unvalidated 302 Location Headers

A background-image updater fetched a configured image URL with manual redirect handling and then re-issued the request to whatever `Location` header came back, with no scheme or host checks. A redirect to `http://169.254.169.254/` or `http://127.0.0.1:<port>/` would have been followed with the original fetch options attached, and the response body written to disk as an image asset. The fix resolves the redirect target against `imgDownloadUrl` and rejects anything that is not HTTPS on the same ho

high

markitdown_bridge.py Path Traversal: Arbitrary File Read via sys.argv

The markitdown_bridge.py script, used by MDView for DOCX-to-Markdown conversion, accepted file paths directly from command-line arguments without validating they stayed within intended directories. An attacker could exploit this to read arbitrary files from the filesystem by passing path traversal sequences in the source_path parameter.

high

SHACL Viewer Path Traversal in graph3d(): Unvalidated `path`

The `graph3d()` and `graph2d()` request handlers in SHACL Viewer directly concatenated user-supplied `path` parameters into filesystem paths, enabling directory traversal outside the intended `/shapes/` directory. The fix introduces `_resolve_shapes_path()` with `os.path.realpath()` validation to enforce containment within the shapes directory.

high

fs.readFileSync(process.argv[2]) Path Traversal in Zola Build

A build-time helper that extracts the expected SHA-256 for a downloaded Zola release passed `process.argv[2]` straight into `fs.readFileSync()` with no directory constraint, so any caller able to influence that argument could make the integrity check read an arbitrary file. The fix resolves the requested path and requires it to be a direct child of the tools directory, which is now passed in as an extra argument, and exits with an error otherwise. Because the bytes read become the "expected" che

high

boardcards.js parseArgs() SSRF: --board Flag Reaches Metadata IPs

The `parseArgs()` function in boardcards.js accepted a user-controlled `--board` command-line argument and passed it directly to fetch() calls without validating the URL scheme or hostname. An attacker could supply `http://169.254.169.254/latest/meta-data/` or other internal addresses to extract cloud credentials or scan internal networks.