skip to content

How do you prevent path traversal in Java when building a file path from user-supplied input?

level: middleimportance: must knowfreq 70%

answer

  1. ../ (and encodings/symlinks/absolute) escape the base dir
  2. Canonicalize first: toRealPath / getCanonicalPath
  3. Then check target.startsWith(base) (path components, not string)
  4. resolve() returns an absolute user path as-is -> check after
  5. Best fix: generated names / allow-list, never user-controlled path

basics

~10 s

Don't trust the input filename. Resolve the full real path (Path.normalize or File.getCanonicalPath), then check it actually stays inside your intended base directory before opening it. Reject anything that escapes, like ../ sequences.

solid answer

~40 s

Path traversal is when input like "../../etc/passwd" makes your code read or write files outside the directory you intended. The fix is canonicalization plus a base-directory containment check. Resolve the candidate path against a fixed base, then collapse it: Path base = Paths.get("/srv/uploads").toRealPath(); Path target = base.resolve(userName).normalize(); and verify target.startsWith(base). With java.io you compare File.getCanonicalPath() of the target against the base's canonical path. getCanonicalPath/toRealPath resolve .., ., and symlinks to an absolute real path so you compare apples to apples — string-matching on the raw input is not enough (encodings, symlinks, and absolute inputs defeat it). Reject absolute user paths and null bytes. Defense in depth: store uploads under generated names, run with least file privileges, and prefer an allow-list of permitted filenames when feasible.

code

java · 10 lines
java
Path base = Paths.get("/srv/uploads").toRealPath();   // real, absolute base
Path target = base.resolve(userName).normalize();     // join + collapse ..

// containment check (path components, not string prefix)
if (!target.startsWith(base)) {
    throw new SecurityException("path traversal blocked: " + userName);
}
// to also defeat symlinks of an existing target:
// target = target.toRealPath();
try (InputStream in = Files.newInputStream(target)) { /* serve file */ }

go deeper

for a junior

Recognizes ../ as the attack and knows you must check the resolved path stays in the intended folder.

for a middle

Implements canonicalize-then-startsWith correctly with toRealPath/getCanonicalPath and knows raw-string checks are insufficient.

for a senior

Covers symlinks vs normalize, absolute-input bypass, encodings, and prefers indirection/allow-list + least privilege.

for a principal

Drives a systemic policy: opaque resource ids, OS-level confinement, and treats traversal as one instance of resolve-then-prove-containment validation.

## The vulnerability: path traversal (a.k.a. directory traversal) Many apps build a filesystem path from user input — a download endpoint, an upload name, a template loader. **Path traversal** is when an attacker supplies special path components so the resolved path points *outside* the directory you meant to confine them to, letting them read or overwrite arbitrary files. The key tool is the **relative path component `..`** ("parent directory"). Given: ```java File f = new File("/srv/uploads/" + name); // name from the user ``` if `name` is `../../etc/passwd`, the resulting path resolves to `/etc/passwd`. Variants include absolute paths (`/etc/passwd`), URL-encoded dots (`%2e%2e%2f`), Windows separators (`..\\`), and **symlinks** that point elsewhere. This is, again, a **trust-boundary** failure: untrusted text is used to address a resource without confirming it stays inside the allowed region. ## Why naive checks fail - **Blacklisting `..`** is fragile: encodings (`%2e%2e`), mixed separators, and overlong forms slip through; and an absolute input doesn't even need `..`. - **String-prefix checking the *raw* input** is wrong because the raw string isn't the real location: `.`/`..` haven't been collapsed and **symlinks** haven't been resolved. A path that *textually* starts with your base can still resolve elsewhere via a symlink. ## The fix: canonicalize, then check containment The robust pattern has two steps: 1. **Canonicalize** the candidate path to its single, absolute, real form — resolving `.`, `..`, and symlinks. 2. **Verify containment**: the canonical target must be *inside* your canonical base directory. **NIO (preferred):** ```java Path base = Paths.get("/srv/uploads").toRealPath(); // real, absolute base Path target = base.resolve(userName).normalize(); // join + collapse .. if (!target.startsWith(base)) { // containment check throw new SecurityException("path traversal blocked"); } // optional: target = target.toRealPath(); // also resolve symlinks of the target ``` - `resolve` joins the user name onto the base (and conveniently, if `userName` is *absolute*, `resolve` returns it as-is — which is exactly why the containment check afterward is essential). - `normalize()` collapses `..`/`.` lexically. - `startsWith(base)` is a **path-component** comparison (not a string prefix), so `/srv/uploads-evil` does not count as inside `/srv/uploads`. - `toRealPath()` additionally resolves symlinks (it requires the file to exist); use it on the target when you must defeat symlink tricks. **Legacy java.io:** ```java File base = new File("/srv/uploads").getCanonicalFile(); File target = new File(base, userName).getCanonicalFile(); if (!target.toPath().startsWith(base.toPath())) { throw new SecurityException("path traversal blocked"); } ``` `getCanonicalPath()/getCanonicalFile()` resolve `..`, `.`, and symlinks to an absolute path (and can throw `IOException`). Compare the **canonical** target against the **canonical** base — never the raw strings. ## Hardening / defense in depth - **Reject absolute inputs and null bytes** up front; reject empty names. - **Prefer indirection:** store uploads under server-generated names (a UUID) and map a public id → real file, so the user never controls the path at all. This is the strongest option. - **Allow-list** when the set of files is known (e.g. a fixed list of report templates). - **Least privilege:** the process should only have filesystem rights to the intended directory, so even a bypass is contained. On modern JDKs the old SecurityManager is deprecated/removed, so rely on OS-level confinement (containers, file permissions). ## The general principle Validate a resource reference by **resolving it to its real identity and proving it lies within an explicit allowed region** — don't try to recognize "bad" inputs by pattern. Same spirit as binding parameters for SQL and argument vectors for commands: control the channel, don't chase the payload.

  • Why isn't normalize() alone sufficient — when do you need toRealPath()?
    normalize() only collapses '.'/'..' lexically; it does not resolve symbolic links. A symlink inside the base could point outside it, so for untrusted, security-critical access resolve the real path with toRealPath() (or getCanonicalPath) before the containment check.
  • What's a design that removes the traversal risk entirely?
    Don't let the user supply the path. Store files under server-generated identifiers (e.g. UUID filenames) and look up the real path from a database by an opaque id, so user input never participates in path construction.

saying these in an interview costs you the question

  • Blacklisting the literal string '..' as the whole defense
  • Comparing the RAW input string to the base instead of the canonicalized path
  • Forgetting that an absolute user path needs no '..' to escape
  • Ignoring symlinks (normalize alone doesn't resolve them; use toRealPath)

context