flawopen.com/Path Traversal/JavaScript

Path Traversal in JavaScript

High Severity CWE-22
ELI5

A file download script is told: 'Read a file inside /uploads/'. If a user asks for '../../package.json', the computer steps back two folders, out of /uploads/ and into the root application folder, reading your source code and API keys.

Key terms on this page
path.resolve vs path.join
path.join simply concatenates path segments, whereas path.resolve processes segments from right to left until an absolute path is formed.
boundary delimiter
Appending path.sep to the base directory path during prefix checking to prevent partial directory matching (e.g. /uploads_backup vs /uploads).

What's happening

In Node.js, path.join(__dirname, 'public', req.query.file) normalizes relative dots but does NOT constrain the result to the public directory. If the query string contains ../../../../etc/passwd, the resulting path points outside the webroot.

Real-world impact

In 2024, path traversal vulnerabilities in multiple Node.js static server packages and Git CLI integrations allowed remote unauthenticated attackers to exfiltrate .env credentials and private SSH keys.

CISA Cybersecurity Advisory & MITRE CVE repository.

Vulnerable vs. fixed

VULNERABLE
// VULNERABLE: path.join allows directory breakout
const express = require('express');
const path = require('path');
const fs = require('fs');

const app = express();
const PUBLIC_DIR = path.join(__dirname, 'public');

app.get('/download', (req, res) => {
  // Attacker input: "../../etc/passwd"
  const targetFile = path.join(PUBLIC_DIR, req.query.file);
  
  // Directly pipes arbitrary system file to response!
  res.sendFile(targetFile);
});
FIXED
// HARDENED: Canonicalize path and assert prefix with directory separator
const express = require('express');
const path = require('path');
const fs = require('fs');

const app = express();
const PUBLIC_DIR = path.resolve(__dirname, 'public');

app.get('/download', (req, res) => {
  const userInput = req.query.file;
  if (!userInput || typeof userInput !== 'string') {
    return res.status(400).send('Invalid file parameter');
  }

  // 1. Resolve to absolute path
  const safePath = path.resolve(PUBLIC_DIR, userInput);

  // 2. Strict boundary check: must start with base folder + separator
  if (!safePath.startsWith(PUBLIC_DIR + path.sep)) {
    return res.status(403).send('Forbidden: Path Traversal detected');
  }

  // 3. Verify file exists and is a regular file
  fs.stat(safePath, (err, stats) => {
    if (err || !stats.isFile()) return res.status(404).send('File not found');
    res.sendFile(safePath);
  });
});

Why the fix works

path.resolve() resolves all .. sequences to an absolute canonical string. Checking that safePath.startsWith(PUBLIC_DIR + path.sep) guarantees the target file is strictly inside the folder and prevents sibling directory confusion.

Gotchas

Omitting path.sep in startsWith

Checking safePath.startsWith(PUBLIC_DIR) allows access to /var/www/public_secret when PUBLIC_DIR is /var/www/public. Always append path.sep.

res.sendFile root option gotchas

Passing { root: PUBLIC_DIR } in Express res.sendFile automatically restricts traversal, but using fs.readFile manually does not.

Common misconceptions

"path.normalize() prevents traversal"

path.normalize() only resolves relative dots; it does not check if the resolved path stepped above the base directory.

How to check if you're affected

grep -rn "path\.join.*req\." --include="*.js" . # ESLint security rule: npm i -D eslint-plugin-security

Prevention checklist

FAQ

Does encodeURIComponent protect against traversal?

No. Attackers send raw or doubly-encoded traversal strings that web frameworks automatically decode before passing to route handlers.

References