Directory Traversal

Path TraversalFile DisclosureLocal File Inclusion

Directory Traversal at a glance

What it is: User controlled paths escape an intended directory and access files elsewhere on the server.
Why it happens: Path traversal occurs when applications accept file/path parameters and pass them to filesystem APIs without canonicalization, validation, or containment checks, allowing attackers to access unauthorized files via crafted paths.
How to fix: Reject raw paths, use allowlisted identifiers, canonicalize and verify targets stay within a fixed base, and use framework helpers for safe file access.

Overview

Directory Traversal happens when an application uses user supplied paths to read or include files. Joining a base directory with untrusted input does not stop .. segments, absolute paths, URL encoded traversal, or symlink jumps. Attackers can read sensitive files like environment configs, keys, or application source. If the path lands in a dynamic include, the bug can turn into code execution.

sequenceDiagram participant Browser participant App as App Server participant FS as Filesystem Browser->>App: GET /log?file=../../etc/passwd App->>FS: readFile(BASE + '../../etc/passwd') FS-->>App: Returns contents of system file App-->>Browser: 200 OK with sensitive data
A potential flow for a Directory Traversal exploit

Where it occurs

It occurs in endpoints that read user-supplied file paths, such as downloaders, log viewers, or template loaders, and pass them to filesystem APIs without proper validation or path containment checks.

Impact

Sensitive data exposure such as credentials, API keys, and source code. In some stacks, traversal enables Local File Inclusion that runs server side code or reveals template files that help other exploits. Disclosure often provides enough information for lateral movement.

Prevention

Prevent path traversal by avoiding user-supplied paths, using IDs or allowlisted filenames, normalizing and verifying paths stay within a fixed base, rejecting unsafe patterns, using safe framework helpers, and storing files outside the web root.

Examples

Switch tabs to view language/framework variants.

Express, reads a file path from a query and sends it

Handler joins a base directory with a user provided path, but never normalizes and checks containment.

Vulnerable
JavaScript • Express — Bad
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
const BASE = '/srv/app/logs';
app.get('/log', (req, res) => {
  const p = path.join(BASE, req.query.file); // BUG: no normalize or containment check
  fs.readFile(p, 'utf8', (err, data) => {
    if (err) return res.status(404).end();
    res.type('text/plain').send(data);
  });
});
  • Line 7: path.join alone does not prevent ../ traversal

Joining a base with user input does not stop .. segments or absolute paths, which lets attackers read arbitrary files.

Secure
JavaScript • Express — Good
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
const BASE = '/srv/app/logs';
app.get('/log', (req,res)=>{
  const name = String(req.query.file||'');
  if (!/^[a-zA-Z0-9_.-]{1,64}$/.test(name)) return res.status(400).send('bad');
  const absBase = fs.realpathSync.native(BASE);
  const target = fs.realpathSync.native(path.join(absBase, name));
  if (!target.startsWith(absBase + path.sep)) return res.status(400).send('bad');
  fs.readFile(target,'utf8',(err,data)=> err?res.status(404).end():res.type('text/plain').send(data));
});
  • Line 10: Resolve to canonical path and enforce base containment
  • Line 8: Strict filename allow list avoids path segments

Map requests to a small allow listed set of filenames, or normalize to a real path and verify it stays under a fixed base.

Engineer Checklist

  • Serve files by ID or allow listed filename, not raw paths

  • Normalize to a real path and verify it starts with the fixed base

  • Reject inputs containing separators or failing a strict filename regex

  • Prefer safe_join or send_from_directory in web frameworks

  • Keep sensitive files outside of web root and static serve paths

End-to-End Example

A support log viewer accepts a file query and reads from disk. An attacker requests a system file using .. segments. The app joins the base and user path but does not check the canonical path, so the read succeeds.

Vulnerable
JAVASCRIPT
// Node.js/Express - Vulnerable directory traversal

app.get('/logs', (req, res) => {
  const filename = req.query.file;
  const BASE = '/var/app/logs';
  
  // VULNERABLE: path.join() doesn't prevent traversal!
  // Attacker sends: ?file=../../etc/passwd
  // Result: /var/app/logs/../../etc/passwd resolves to /etc/passwd
  const filepath = path.join(BASE, filename);
  
  fs.readFile(filepath, 'utf8', (err, data) => {
    if (err) {
      return res.status(404).send('File not found');
    }
    res.type('text/plain').send(data);
  });
});

// ALSO VULNERABLE: Image server
app.get('/images', (req, res) => {
  const imageName = req.query.img;
  const IMAGE_DIR = '/var/app/public/images';
  
  // VULNERABLE: No validation on image name
  // Attacker sends: ?img=../../../app/.env
  const imagePath = path.join(IMAGE_DIR, imageName);
  
  res.sendFile(imagePath, (err) => {
    if (err) {
      res.status(404).send('Image not found');
    }
  });
});

// VULNERABLE: Download endpoint
app.get('/download', (req, res) => {
  const documentPath = req.query.path;
  const DOCS_BASE = '/var/app/documents';
  
  // VULNERABLE: Using user-supplied path directly
  // Attacker sends: ?path=/etc/passwd (absolute path bypasses join)
  // Or: ?path=../../../home/app-user/.ssh/id_rsa
  const fullPath = path.join(DOCS_BASE, documentPath);
  
  fs.access(fullPath, fs.constants.R_OK, (err) => {
    if (err) {
      return res.status(404).send('Document not found');
    }
    
    // VULNERABLE: Reads and sends ANY accessible file
    res.download(fullPath);
  });
});

// VULNERABLE: Template loader (can lead to code execution)
app.get('/page', (req, res) => {
  const pageName = req.query.name || 'home';
  const TEMPLATES_DIR = '/var/app/views';
  
  // VULNERABLE: Dynamic template loading with user input
  // Attacker sends: ?name=../../app.js
  // If using certain template engines, this can execute code
  const templatePath = path.join(TEMPLATES_DIR, pageName + '.html');
  
  fs.readFile(templatePath, 'utf8', (err, content) => {
    if (err) {
      return res.status(404).send('Page not found');
    }
    
    // Even just reading source code exposes logic and secrets
    res.send(content);
  });
});
Secure
JAVASCRIPT
// Node.js/Express - Secure file access with path validation

// SECURE: Log viewer with strict validation
app.get('/logs', (req, res) => {
  const filename = req.query.file || '';
  const BASE = '/var/app/logs';
  
  // 1. Validate filename format - only simple filenames, no path separators
  if (!/^[a-zA-Z0-9_.-]{1,64}$/.test(filename)) {
    return res.status(400).json({ error: 'Invalid filename format' });
  }
  
  // 2. Resolve to canonical absolute path
  const absBase = path.resolve(BASE);
  const targetPath = path.resolve(absBase, filename);
  
  // 3. Verify the resolved path is still within base directory
  // This catches ../ traversal and symlink attacks
  if (!targetPath.startsWith(absBase + path.sep)) {
    return res.status(403).json({ error: 'Access denied' });
  }
  
  // 4. Check file exists and is readable
  fs.access(targetPath, fs.constants.R_OK, (err) => {
    if (err) {
      return res.status(404).json({ error: 'File not found' });
    }
    
    fs.readFile(targetPath, 'utf8', (err, data) => {
      if (err) {
        return res.status(500).json({ error: 'Read error' });
      }
      res.type('text/plain').send(data);
    });
  });
});

// SECURE: Use ID mapping instead of file paths
const ALLOWED_IMAGES = {
  'logo': 'company-logo.png',
  'banner': 'hero-banner.jpg',
  'avatar': 'default-avatar.png'
};

app.get('/images', (req, res) => {
  const imageId = req.query.id;
  const IMAGE_DIR = '/var/app/public/images';
  
  // Map ID to filename - user never controls the path
  const filename = ALLOWED_IMAGES[imageId];
  
  if (!filename) {
    return res.status(404).json({ error: 'Image not found' });
  }
  
  // Safe: filename comes from our mapping, not user input
  const imagePath = path.join(IMAGE_DIR, filename);
  res.sendFile(imagePath);
});

// SECURE: Download with whitelist validation
const ALLOWED_DOCUMENTS = new Set([
  'user-guide.pdf',
  'terms.pdf',
  'privacy-policy.pdf',
  'api-docs.pdf'
]);

app.get('/download', (req, res) => {
  const documentName = req.query.doc;
  const DOCS_BASE = '/var/app/documents';
  
  // Whitelist check - only allow specific documents
  if (!ALLOWED_DOCUMENTS.has(documentName)) {
    return res.status(403).json({ error: 'Document not available' });
  }
  
  // Additional validation: no path separators
  if (documentName.includes('/') || documentName.includes('\\')) {
    return res.status(400).json({ error: 'Invalid document name' });
  }
  
  const fullPath = path.join(DOCS_BASE, documentName);
  const canonicalPath = path.resolve(fullPath);
  const canonicalBase = path.resolve(DOCS_BASE);
  
  // Double-check containment
  if (!canonicalPath.startsWith(canonicalBase + path.sep)) {
    return res.status(403).json({ error: 'Access denied' });
  }
  
  res.download(canonicalPath);
});

// SECURE: Template system with fixed templates only
const ALLOWED_PAGES = {
  'home': 'home.html',
  'about': 'about.html',
  'contact': 'contact.html',
  'pricing': 'pricing.html'
};

app.get('/page', (req, res) => {
  const pageId = req.query.id || 'home';
  const TEMPLATES_DIR = '/var/app/views';
  
  // Map page ID to template filename
  const templateFile = ALLOWED_PAGES[pageId];
  
  if (!templateFile) {
    return res.status(404).send('Page not found');
  }
  
  // Safe: template path comes from our whitelist
  const templatePath = path.join(TEMPLATES_DIR, templateFile);
  
  fs.readFile(templatePath, 'utf8', (err, content) => {
    if (err) {
      return res.status(500).send('Error loading page');
    }
    res.send(content);
  });
});

Discovery

This vulnerability is discovered by requesting the /log endpoint with path traversal sequences (../) in the file parameter and observing that the application reads files outside the intended directory, such as /etc/passwd or application config files.

  1. 1. Baseline request

    http

    Action

    Request legitimate log file to understand normal behavior

    Request

    GET https://app.example.com/api/logs?file=application.log

    Response

    Status: 200
    Body:
    {
      "content": "[2024-01-15 10:15:32] INFO: Server started\n[2024-01-15 10:15:45] INFO: Database connected\n[2024-01-15 10:16:01] INFO: Request processed successfully",
      "file": "application.log",
      "size": 2048
    }

    Artifacts

    baseline_established log_file_accessed
  2. 2. Test basic path traversal

    http

    Action

    Attempt simple directory traversal with ../ sequences

    Request

    GET https://app.example.com/api/logs?file=../../etc/passwd

    Response

    Status: 200
    Body:
    {
      "content": "root:x:0:0:root:/root:/bin/bash\nbin:x:1:1:bin:/bin:/sbin/nologin\ndaemon:x:2:2:daemon:/sbin:/sbin/nologin\napp-user:x:1000:1000::/home/app-user:/bin/bash\npostgres:x:999:999:PostgreSQL Server:/var/lib/postgresql:/bin/bash\nnginx:x:101:101:nginx user:/var/cache/nginx:/sbin/nologin",
      "note": "Path traversal successful - /etc/passwd file disclosed"
    }

    Artifacts

    path_traversal_confirmed passwd_file_disclosed system_users_enumerated
  3. 3. Test URL-encoded traversal

    http

    Action

    Try URL encoded path traversal to bypass basic filters

    Request

    GET https://app.example.com/api/logs?file=..%2F..%2Fetc%2Fhostname

    Response

    Status: 200
    Body:
    {
      "content": "prod-server-01.internal.example.com\n",
      "note": "URL encoding bypass successful - hostname disclosed"
    }

    Artifacts

    encoding_bypass hostname_disclosed
  4. 4. Extract application secrets

    http

    Action

    Access .env file containing database credentials and API keys

    Request

    GET https://app.example.com/api/logs?file=../../app/.env

    Response

    Status: 200
    Body:
    {
      "content": "DATABASE_URL=postgresql://admin:Pr0dP@ssw0rd2024@db.internal.example.com:5432/production\nREDIS_URL=redis://cache.internal.example.com:6379\nAWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE\nAWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\nSTRIPE_SECRET_KEY=sk_live_51HxYz3FGHxYz3FGHxYz3FG\nJWT_SECRET=super-secret-jwt-key-do-not-share\nSMTP_PASSWORD=smtp_password_123"
    }

    Artifacts

    env_file_disclosed database_credentials aws_keys stripe_key jwt_secret

Exploit steps

An attacker exploits this by crafting file parameters with traversal sequences to read sensitive files like .env, database.yml, or application source code, extracting credentials and secrets that enable further compromise of the system.

  1. 1. Extract application configuration

    Read database.yml configuration file

    http

    Action

    Use path traversal to access database configuration

    Request

    GET https://app.example.com/api/logs?file=../../config/database.yml

    Response

    Status: 200
    Body:
    {
      "content": "production:\n  adapter: postgresql\n  host: db.internal.example.com\n  port: 5432\n  database: production\n  username: admin\n  password: Pr0dP@ssw0rd2024\n  pool: 25\n  timeout: 5000"
    }

    Artifacts

    database_config db_credentials connection_details
  2. 2. Read application source code

    Access main application file

    http

    Action

    Extract source code to identify additional vulnerabilities

    Request

    GET https://app.example.com/api/logs?file=../../app.js

    Response

    Status: 200
    Body:
    {
      "content": "const express = require('express');\nconst db = require('./database');\nconst JWT_SECRET = 'super-secret-jwt-key-do-not-share';\n\napp.get('/admin/users', (req, res) => {\n  // No authorization check!\n  db.query('SELECT * FROM users', (err, users) => {\n    res.json(users);\n  });\n});",
      "note": "Source code reveals hardcoded secrets and missing authorization checks"
    }

    Artifacts

    source_code_disclosure hardcoded_secrets additional_vulnerabilities_found
  3. 3. Access SSH private keys

    Read server SSH keys for lateral movement

    http

    Action

    Attempt to read SSH private keys from typical locations

    Request

    GET https://app.example.com/api/logs?file=../../../home/app-user/.ssh/id_rsa

    Response

    Status: 200
    Body:
    {
      "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAr8Zx1mN+kJ8...\n[SSH private key contents]\n-----END RSA PRIVATE KEY-----",
      "note": "SSH private key exposed - enables direct server access"
    }

    Artifacts

    ssh_private_key lateral_movement_capability server_access_credential
  4. 4. Leverage stolen credentials

    Connect to production database

    cli

    Action

    Use extracted credentials to access database directly

    Request

    Response

    Artifacts

    database_access user_data_exfiltration password_hashes complete_compromise

Specific Impact

Configuration files, keys, and source code can be read. With disclosure of credentials, attackers pivot to databases, queues, or cloud APIs. If dynamic includes are reachable, traversal can escalate to code execution.

The breach undermines trust and increases the blast radius since leaked secrets may allow changes or further data theft.

Fix

Use simple allow listed filenames and verify canonical containment under a fixed base. Add tests for traversal attempts and CI checks that forbid using raw user paths.

Detect This Vulnerability in Your Code

Sourcery automatically identifies directory traversal vulnerabilities and many other security issues in your codebase.

Scan Your Code for Free