# Path traversal prevention

- **Pattern:** `ab-002411` (`security-hardening.secrets-config.path-traversal`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002411
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002411)

## Why it matters

Path traversal (CWE-22, OWASP A01 Broken Access Control) uses `../` sequences to escape an intended directory, allowing an attacker to read files like `/etc/passwd`, `process.env`, or private keys from arbitrary locations on the filesystem. NIST 800-53 SI-10 requires input sanitization before filesystem operations. Any endpoint that accepts a filename parameter — downloads, file preview, static serving — is a potential traversal vector if the path is not normalized and boundary-checked. A URL like `/api/download?file=../../.env` against a naive `path.join` implementation returns the application's secrets.

## Severity rationale

Low because path traversal requires a file-serving endpoint with user-controlled path input, but exploitation yields direct filesystem read access to arbitrary files.

## Remediation

Normalize every user-supplied path with `path.resolve()` and verify it starts with the expected base directory:

```ts
import path from 'path'

const UPLOADS_DIR = path.resolve(process.cwd(), 'uploads')

function safeReadFile(userFilename: string): Buffer {
  const resolved = path.resolve(UPLOADS_DIR, userFilename)

  if (!resolved.startsWith(UPLOADS_DIR + path.sep)) {
    throw new Error('Access denied')
  }

  return require('fs').readFileSync(resolved)
}
```

Store uploaded files with random UUIDs as names (`crypto.randomUUID()`) rather than user-supplied filenames to eliminate traversal entirely. Serve files from external blob storage (Vercel Blob, S3) where path traversal is not applicable.

## Detection

- **ID:** `path-traversal`
- **Severity:** `low`
- **What to look for:** Enumerate every file system access that uses user-supplied input (file reads, downloads, uploads, static serving). For each, search for filesystem operations that include user-controlled values in paths: `fs.readFile`, `fs.writeFile`, `path.join`, `path.resolve` with user input. Check endpoints that serve files by name or download endpoints that accept a filename parameter. Look for `../` sequences or equivalent traversal in path construction.
- **Pass criteria:** Any path constructed from user input is normalized and validated to remain within an expected base directory. `path.resolve()` is used and the result is verified to start with the expected directory prefix — 100% of path operations must normalize and validate against directory traversal. Report: "X file access points with user input found, all Y sanitize paths."
- **Fail criteria:** User-controlled filenames or paths are used directly in filesystem operations without sanitization or boundary checking.
- **Skip (N/A) when:** The application performs no filesystem operations based on user input.
- **Detail on fail:** `"GET /api/download?file= parameter used directly in fs.readFile without path normalization — directory traversal possible"` or `"File serving endpoint constructs path with user-supplied filename without checking it stays within uploads directory"`
- **Remediation:** Normalize and validate all file paths:

  ```ts
  import path from 'path'

  const UPLOADS_DIR = path.resolve(process.cwd(), 'uploads')

  function safeReadFile(userFilename: string): string {
    // Normalize the path (resolves .., removes duplicates)
    const resolved = path.resolve(UPLOADS_DIR, userFilename)

    // Verify the resolved path is within the expected directory
    if (!resolved.startsWith(UPLOADS_DIR + path.sep)) {
      throw new Error('Access denied: path traversal attempt detected')
    }

    return require('fs').readFileSync(resolved, 'utf8')
  }
  ```

## External references

- cwe CWE-22
- owasp A01
- nist SI-10

Taxons: injection-and-input-trust

HTML version: https://auditbuffet.com/patterns/ab-002411
