How do I fix path traversal?

Direct answer

Resolve the final path and confirm it sits inside the directory you intended, rather than stripping `../` from the input. Stripping fails on encoded and nested variants. Better still, don't build paths from user input at all. Map an identifier to a filename you control.

Muhammad HasanUpdated

Path traversal is when user input reaches a filesystem call and escapes the directory you meant to serve from.

// Vulnerable
app.get('/files/:name', (req, res) => {
  res.sendFile(path.join('/var/uploads', req.params.name));
});

Request ../../etc/passwd and path.join resolves it obligingly.

Don't strip, resolve and verify

The common attempt is to remove ../ from the input. It fails, and predictably:

  • ....//, strip the inner ../ and you're left with ../

  • %2e%2e%2f. URL-encoded, decoded after your check

  • ..\\ on Windows

  • Double-encoded variants

Resolve the path and check where it landed:

const path = require('path');
const BASE = path.resolve('/var/uploads');

function safePath(input) {
  const full = path.resolve(BASE, input);
  if (full !== BASE && !full.startsWith(BASE + path.sep)) {
    throw new Error('Outside base directory');
  }
  return full;
}

The + path.sep matters. A plain startsWith(BASE) accepts /var/uploads-public, which is a different directory.

Better: don't use user input as a path

If you can avoid constructing paths from input, do.

const file = await db.files.findOne({
  id: req.params.id,
  userId: req.user.id      // ownership, while you're here
});
res.sendFile(path.join(BASE, file.storedName));

The user supplies an identifier. You supply the filename. There's nothing to traverse.

This also solves a problem the path check doesn't: a resolved path can be perfectly inside your uploads directory and still belong to someone else. Traversal and authorisation are separate bugs and the second one is easy to miss while fixing the first.

Symlinks

path.resolve works on strings and knows nothing about the filesystem. A symlink inside your base directory pointing outside it passes the check.

If users can create files in that directory, use fs.realpath and check the result instead.

Uploads

Never use a client-supplied filename on disk. Generate your own, a UUID. And keep the original in the database for display. That removes traversal, collisions and a set of problems around unusual characters at the same time.

Where to look

This class has a recognisable shape, user input flowing into a filesystem call. So dataflow-based tools generally catch it. Grep for readFile, sendFile, createReadStream and unlink and check what reaches them.

Go deeper

Fast remediation with Kolega

Related answers

See what your own repository returns

Connect a repo and run a scan. No credit card, no pipeline changes.

Get started for free