Path traversal

Path traversal

Description

Path traversal lets an attacker use components such as ../ in a file path to access unauthorized files or directories. It commonly occurs when unvalidated input reaches filesystem functions, potentially exposing or overwriting sensitive information or system files.

Potential impact

  • Unauthorized access to important system files or other users' data
  • Modification or deletion of important files
  • Service disruption if essential files are damaged

Remediation

  • Check input for path manipulation strings such as ../, /, and \.
  • Restrict file access to a designated directory and limit absolute paths.
  • Normalize paths, for example with Node.js path.normalize(), and verify that the result stays within the base directory.
  • Allow only approved filenames or predefined filename rules.
  • Distinguish APIs whose first argument is a path, such as fs.writeFile(path, data), from those taking a file descriptor, such as fs.write(fd, data). Input used only as file content or as data after a descriptor needs a different assessment from path traversal.

Examples

Before

javascript
const fs = require('fs');
const express = require('express');
const app = express();

app.get('/download', (req, res) => {
  // Before: use input directly in a filesystem call.
  const fileName = req.query.file;
  fs.readFile(fileName, (err, data) => {
    if (err) return res.status(404).send('File not found');
    res.send(data);
  });
});

After

javascript
const fs = require('fs');
const path = require('path');
const express = require('express');
const app = express();

// Only the server may write here, preventing link replacement between checking and reading.
const BASE_DIR = fs.realpathSync(path.join(__dirname, 'files'));

function isInside(baseDir, targetPath) {
  const relative = path.relative(baseDir, targetPath);
  return (
    relative !== '' &&
    relative !== '..' &&
    !relative.startsWith('..' + path.sep) &&
    !path.isAbsolute(relative)
  );
}

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

  // Allow filenames only; reject directory components instead of stripping them.
  const fileName = path.basename(rawName);
  if (fileName !== rawName || !/^[a-zA-Z0-9._-]+$/.test(fileName)) {
    return res.status(400).send('Invalid file path');
  }

  const candidate = path.resolve(BASE_DIR, fileName);
  if (!isInside(BASE_DIR, candidate)) {
    return res.status(400).send('Invalid file path');
  }

  let requestedPath;
  try {
    requestedPath = fs.realpathSync(candidate);
  } catch {
    return res.status(404).send('File not found');
  }
  if (!isInside(BASE_DIR, requestedPath)) {
    return res.status(400).send('Invalid file path');
  }

  fs.readFile(requestedPath, (err, data) => {
    if (err) return res.status(404).send('File not found');
    res.send(data);
  });
});

app.post('/message', (req, res) => {
  // The server fixes the path; input supplies file contents only.
  fs.writeFile(path.join(BASE_DIR, 'message.txt'), req.body.message, (err) => {
    if (err) return res.status(500).send('write failed');
    res.send('saved');
  });
});

Explanation:

  • Before: fileName reaches fs.readFile without validation, allowing path components such as ../. An attacker may access files the process has permission to read.
  • After: Check the string type and equality with path.basename() to reject directory components. Verify that both the normalized path and the real path after symbolic-link resolution remain inside the server-managed BASE_DIR. Input used only as file contents, with a server-fixed path, is a separate concern from path traversal.

The POST excerpt assumes request-body parsing middleware has been configured.

References