Back to Blog
critical SEVERITY6 min read

How Path Traversal Vulnerabilities Happen in Node.js Build Scripts and How to Fix It

A critical path traversal vulnerability in `scripts/build-all.js` allowed attackers to escape the intended output directory by supplying crafted command-line arguments like `--output ../../../../etc/passwd`. The fix validates that the resolved output path remains within the repository root, preventing unauthorized file system access.

O
By Orbis AppSec
Published August 16, 2026Reviewed August 16, 2026

Answer Summary

Path traversal in Node.js occurs when user-supplied file paths aren't validated before use in file operations, allowing attackers to escape intended directories using relative path sequences like `../`. In `build-all.js`, the `--output` argument was resolved without checking if the final path stayed within bounds. The fix adds a validation check using `path.startsWith()` to ensure the resolved output path remains within the repository root directory.

Vulnerability at a Glance

cweCWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
fixAdded path boundary validation before file operations
riskAttackers can read, write, or delete arbitrary files on the system
languageJavaScript (Node.js)
root causeUnvalidated path resolution of user-supplied `--output` argument
vulnerabilityPath Traversal / Directory Escape

How Path Traversal Vulnerabilities Happen in Node.js Build Scripts and How to Fix It

Introduction

In the scripts/build-all.js file, a seemingly innocent command-line argument handler created a critical security gap. The script accepted an --output parameter to specify where built artifacts should be written, but it never verified that the resolved path actually stayed within the intended output directory. This allowed attackers to escape the sandbox and access arbitrary locations on the filesystem—a classic path traversal vulnerability.

Consider this attack scenario: An attacker could execute:

npm run build:paper -- --output ../../../../etc/passwd

Or on Windows:

npm run build:paper -- --output C:\Windows\System32\config\SAM

The vulnerable code would happily resolve these paths and attempt file operations outside the repository, potentially overwriting critical system files or exfiltrating sensitive data.

The Vulnerability Explained

Let's examine the vulnerable code from scripts/build-all.js:

const outputArg = process.argv.includes('--output') ? process.argv[process.argv.indexOf('--output') + 1] : 'site/papers';
const outputRoot = path.resolve(ROOT, outputArg);
// ... rest of script uses outputRoot for file operations

The problem: The code extracts the --output argument directly from process.argv and resolves it relative to ROOT. However, path.resolve() has a subtle behavior: if the second argument is an absolute path, it ignores the first argument entirely.

Here's how the attack works:

  1. User provides: --output /etc/passwd
  2. Code calls: path.resolve(ROOT, '/etc/passwd')
  3. Result: path.resolve() returns /etc/passwd (the absolute path wins)
  4. Impact: The script now writes to a system file instead of the repository

Even with relative paths, the traversal works:

  1. User provides: --output ../../../../etc/passwd
  2. Code calls: path.resolve(ROOT, '../../../../etc/passwd')
  3. Result: path.resolve() normalizes the path and escapes ROOT
  4. Impact: Files are written outside the intended directory

The real-world impact is severe: in a CI/CD pipeline, this could allow an attacker to:
- Overwrite application code
- Inject malicious build artifacts
- Exfiltrate source code or secrets
- Corrupt the build environment
- Escalate privileges by modifying system files (if running with elevated permissions)

The Fix

The security team added a simple but critical validation check immediately after path resolution:

const outputArg = process.argv.includes('--output') ? process.argv[process.argv.indexOf('--output') + 1] : 'site/papers';
const outputRoot = path.resolve(ROOT, outputArg);

// NEW: Validate that the output path stays within ROOT
if (outputRoot !== ROOT && !outputRoot.startsWith(ROOT + path.sep)) {
  throw new Error(`--output path must be within the repository root: ${outputRoot}`);
}

const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';

How this fix works:

  1. outputRoot !== ROOT: Allows the output directory to be ROOT itself (valid use case)
  2. !outputRoot.startsWith(ROOT + path.sep): Ensures the resolved path starts with the ROOT directory followed by a path separator (preventing partial string matches like /repo-evil matching /repo)
  3. Throws an error: If validation fails, the script terminates immediately with a clear error message

The path.sep is crucial—without it, a check like outputRoot.startsWith(ROOT) would incorrectly allow /repo-evil/output if ROOT was /repo.

Why this specific approach:

  • Simple and fast: No filesystem calls or complex logic
  • Cross-platform: Works on Windows (\) and Unix (/) due to path.sep
  • Fail-secure: Rejects suspicious paths rather than trying to sanitize them
  • Clear error message: Developers immediately understand what went wrong

Prevention & Best Practices

To avoid path traversal vulnerabilities in your Node.js applications:

1. Always Validate User-Supplied Paths

// ❌ BAD: No validation
const userPath = getUserInput();
fs.readFile(userPath, callback);

// ✅ GOOD: Validate boundaries
const basePath = path.resolve(__dirname, 'uploads');
const userPath = path.resolve(basePath, userInput);
if (!userPath.startsWith(basePath + path.sep)) {
  throw new Error('Invalid path');
}
fs.readFile(userPath, callback);

2. Use path.relative() for Verification

An alternative approach uses path.relative() to ensure the path doesn't escape:

const basePath = path.resolve(__dirname, 'uploads');
const userPath = path.resolve(basePath, userInput);
const relative = path.relative(basePath, userPath);

// If relative path starts with '..', it escaped the base
if (relative.startsWith('..')) {
  throw new Error('Path traversal detected');
}

3. Implement Whitelist-Based Approach

For maximum security, use a whitelist of allowed paths:

const ALLOWED_OUTPUTS = ['site/papers', 'dist', 'build'];
if (!ALLOWED_OUTPUTS.includes(outputArg)) {
  throw new Error(`Output must be one of: ${ALLOWED_OUTPUTS.join(', ')}`);
}

4. Use Static Analysis Tools

Enable security-focused linters and SAST tools:

  • ESLint with eslint-plugin-security
  • Semgrep with path traversal rules
  • Snyk for dependency and code scanning
  • SonarQube for comprehensive SAST analysis

5. Principle of Least Privilege

  • Run build scripts with minimal file system permissions
  • Use containerization to isolate build environments
  • Avoid running builds as root or with admin privileges

6. Input Validation Standards

Follow OWASP guidelines:
- Reject rather than sanitize (blacklists are incomplete)
- Use allowlists when possible
- Validate type, length, format, and range
- Encode output appropriately for the context

Key Takeaways

  • Never trust path.resolve() alone: It normalizes paths but doesn't enforce boundaries—absolute paths bypass the base directory entirely.
  • Always validate resolved paths: Check that path.startsWith(basePath + path.sep) to prevent partial string matches and directory escape.
  • The --output argument in build scripts is high-risk: Build automation is a prime attack target; validate all command-line arguments that affect file operations.
  • Use path.sep in boundary checks: Prevents edge cases where /repo could match /repo-evil; always include the separator.
  • Fail-secure, not fail-open: When path validation fails, throw an error immediately rather than attempting to sanitize or guess the user's intent.

How Orbis AppSec Detected This

Source: The --output command-line argument extracted from process.argv in scripts/build-all.js:9

Sink: The path.resolve(ROOT, outputArg) call that creates outputRoot without validating its boundaries

Missing control: No validation that the resolved outputRoot path remains within the ROOT directory; no check for absolute paths or path traversal sequences

CWE: CWE-22 - Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

Fix: Added a conditional check immediately after path resolution to verify that outputRoot either equals ROOT or starts with ROOT + path.sep, throwing an error if the path escapes the intended directory.

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

Path traversal vulnerabilities are among the most exploitable file system issues because they're often overlooked during code review. The fix in build-all.js demonstrates that robust security requires just a few lines of validation code—but those lines must be present before any file operations.

The lesson here extends beyond this specific script: any user-controlled input that influences file paths must be validated. Whether it's a command-line argument, HTTP request parameter, or configuration file, the same principle applies: resolve the path, validate the boundary, and fail securely if the path escapes.

By adopting these practices—boundary validation, fail-secure error handling, and continuous static analysis—you can eliminate an entire class of vulnerabilities from your Node.js applications.

References

Frequently Asked Questions

What is path traversal in Node.js?

Path traversal occurs when a Node.js application accepts user-controlled file paths without validating that the resolved path stays within an intended directory. Attackers exploit this by using `../` sequences or absolute paths to access files outside the intended boundary.

How do you prevent path traversal in Node.js build scripts?

Always validate user-supplied paths by resolving them and checking that the resolved path starts with the intended base directory. Use `path.resolve()` followed by a `startsWith()` check or `path.relative()` to ensure the path doesn't escape the boundary.

What CWE is path traversal?

Path traversal is CWE-22: "Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')". It's one of the most common file system vulnerabilities.

Is using path.resolve() alone enough to prevent path traversal?

No. `path.resolve()` normalizes the path but doesn't enforce boundaries. You must also validate that the resolved path stays within the intended directory using `startsWith()` or similar checks.

Can static analysis detect path traversal?

Yes. Modern static analysis tools like Semgrep, ESLint security plugins, and SAST scanners can detect when user-controlled input is passed to file system functions without validation.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #7

Related Articles

critical

How Path Traversal Vulnerabilities Happen in Node.js Development Servers and How to Fix Them

A critical path traversal vulnerability was discovered in the development file server script `serve.mjs`, where arbitrary directory paths from command-line arguments were accepted without validation. This flaw could allow attackers to serve any directory on the filesystem over HTTP, potentially exposing sensitive system files like `/etc/passwd` or application secrets. The fix adds a simple but effective validation check ensuring the serve root stays within the current working directory.

high

How Path Traversal and Security Policy Bypass Happens in Node.js Dependencies and How to Fix It

A high-severity vulnerability in the fast-uri package (CVE-2026-6321) allowed attackers to bypass security policies through improper Unicode hostname canonicalization and path traversal. This issue affected the @apralabs/apra-fleet project through its dependency tree, and was resolved by upgrading fast-uri from version 3.1.0 to 4.1.2 using npm overrides.

high

How path traversal happens in Python open() and how to fix it

A high-severity path traversal vulnerability was discovered in `src/backend/snitch.py` where the `writeTestcase()` function accepted a user-controlled `portDir` parameter without sanitization. An attacker could craft malicious input like `../../etc` to write files outside the intended output directory. The fix implements path canonicalization using `pathlib.Path.resolve()` and validates that the final destination stays within the allowed base directory.

high

How URL-Encoded Path Traversal happens in Python nltk.data.load() and how to fix it

CVE-2026-54293 is a high-severity path traversal vulnerability in NLTK's `nltk.data.load()` function that allows attackers to read arbitrary local files by supplying URL-encoded path sequences. The fix pins NLTK to version 3.10.0 or later via a constraint dependency in `pyproject.toml`, preventing the vulnerable version from being resolved transitively through `rouge-score` and `lm-eval`. Because this project is a web service, the vulnerability was directly exploitable by remote attackers withou

high

How path traversal happens in Ruby YARD server and how to fix it

A high-severity path traversal vulnerability (CVE-2026-41493) in YARD versions prior to 0.9.42 allowed attackers to read arbitrary files from servers running `yard server`. This fix upgrades the yard gem from 0.9.26 to 0.9.42 in the Gemfile and Gemfile.lock, closing a dangerous information disclosure vector that could expose configuration files, credentials, and source code.

critical

How Command Injection via Unescaped Line Terminators Happens in Node.js and How to Fix It

A critical command injection vulnerability (CVE-2026-9277) was discovered in the shell-quote npm package version 1.8.3, where unescaped line terminators could allow attackers to execute arbitrary code. This fix upgrades shell-quote to version 1.9.0 using npm overrides to ensure all instances in the dependency tree are patched, eliminating the attack vector across the entire application.