Injection and output

Keep file paths inside a root

Generate storage names, resolve canonical paths, and prevent user-controlled traversal outside the directory an operation owns.

A filename like ../../secrets.env is data to the user and a path instruction to the filesystem. The .. segments walk up the directory tree. Treating those two meanings as equivalent is how path traversal starts.

The weak check compares strings before the path is resolved.

// Vulnerable: prefix check on unresolved text
const target = path.join('/srv/reports', req.query.file)
if (target.startsWith('/srv/reports')) fs.createReadStream(target)

A download route joins /srv/reports with ../../.env. A prefix check on the unresolved text passes in one implementation, while a symlink inside the directory escapes later.

Resolve first, then check containment

Resolve the path to its canonical form with fs.realpath, which follows symlinks, and only then confirm it sits inside the root.

const root = await fs.promises.realpath('/srv/reports')
const resolved = await fs.promises.realpath(path.join(root, req.query.file))
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
  return res.status(404).end() // outside the root: refuse, do not reveal
}

Better still, avoid interpreting user path input at all. Store files under generated IDs and map a requested ID to a server-owned path.

const files = { 'q1-report': '/srv/reports/2026-q1.pdf' }
const chosen = files[req.params.id] // unknown IDs simply do not resolve

The same discipline covers writes and deletes, not just downloads. A route that saves an upload or removes a temporary file is just as exposed if it joins user text onto a base directory without resolving and checking the result.

Normalizing dot segments does not solve symlink behavior by itself, because a symlink resolves to a path outside the root after the check has already passed. Server-generated IDs avoid most path interpretation and make the remaining checks smaller.

Test the file route with dot segments, absolute paths, encoded separators, and a symlink that points outside the root. Record the resolved target and prove every escape attempt is denied without revealing the external file.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →