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
•Technically reviewed by Anupam Mediratta•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

critical

path.resolve() Path Traversal in Node CLI's Dynamic import()

A Node.js CLI script for validating expression definitions took a file path from `process.argv[2]`, resolved it with `path.resolve()`, and passed the result straight into a dynamic `import()` — with no check that the resolved path stayed inside the working directory. An attacker (or a malicious skill/plugin invocation) could supply traversal sequences to load and execute arbitrary `.js` files from anywhere on disk. The fix adds a boundary check with `path.relative()` and an extension allowlist b

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

formatObject() Leaves localStorage Keys Unescaped Before innerHTML

The `formatObject()` pretty-printer used to build the key-configuration storage viewer interpolated object keys and array key/value pairs directly into an HTML string that is later assigned with `innerHTML`. Only string *values* were passed through `escapeHtml()`, so a crafted key name persisted in `localStorage` could execute script every time a user opened the storage viewer. The fix wraps the index-pair branch and the object-key branch in `escapeHtml()`, keeping the raw path only for non-colo