Under attack? Get incident response help

Secure development

Path traversal: use IDs, not file names, and check the resolved path

How to find file reads, downloads and includes that accept ../ sequences, and fix them with ID lookups and canonical-path checks instead of blocklists.

Also in:TürkçeDeutschFrançaisEspañolItalianoPolskiNederlandsEesti

Illustration: Path traversal: use IDs, not file names, and check the resolved path

Path traversal exists wherever your code uses external input to build a pathname and does not stop that pathname resolving outside the directory you meant. CWE-22 is the entry for it. Find every such place, stop letting users supply file names, and check the resolved path for whatever input remains.

What an attacker gets

Reading is only the start. Depending on what the code does with the path, an attacker may read unexpected files and expose sensitive data, create or overwrite programs and libraries and so execute code, or corrupt files until the product stops working or locks users out. OWASP lists application source code and configuration among the files that can be reached.

Files behind a security mechanism are the worst targets. Reading a password file allows brute force guessing against accounts, and appending a new account to one can bypass authentication.

Reach depends on the platform. On Windows an attacker can move only within the partition that holds the web root, on Linux across the whole disk. Operating system access controls still apply on top.

Finding the places that build paths

Start from the sink, not the URL. List every call that opens, reads, serves, includes or writes a file, then trace each path argument back to its origin. Anything that touches the request goes on the list, including calls hidden behind your own wrapper functions and framework helpers, so check their callers too.

The input is wider than the query string. The OWASP examples include get-files.jsp?file=report.pdf, get-page.php?home=aaa.html and some-page.asp?page=index.html, and also a cookie. This snippet is adapted from the OWASP (originally Wikipedia) example:

$template = 'blue.php';
if ( isset( $_COOKIE['TEMPLATE'] ) )
    $template = $_COOKIE['TEMPLATE'];
include ( "/home/users/phpguru/templates/" . $template );

A cookie of TEMPLATE=../../../../../../../../../etc/passwd makes include() climb to the root directory and return the password file. Parameters named file, page, template or f are the usual suspects, but names are no guide. Trace the data.

Check three forms:

  • Relative traversal: ../ sequences that climb out of the intended directory.
  • Absolute paths: ?f=/etc/passwd or ?f=/var/www/html/get.php, which work when the input is used as the whole path.
  • Remote includes: ?page=http://other-site/... where the include mechanism accepts URLs.

Verbose error messages help the attacker, because they reveal real paths to guess against. Check what your application returns when a file is missing.

Why blocklists fail

Stripping ../ does not work, because the same traversal has many spellings:

  • %2e%2e%2f, ..%2f and %2e%2e/ all mean ../.
  • ..%5c and %2e%2e%5c mean ..\, which matters on Windows, where both separators work.
  • %252e%252e%255c is the double-encoded form of ..\. Web containers perform one level of decoding, so it arrives still encoded. If your code or a library decodes it again, it becomes traversal after the check has run.
  • ..%c0%af and ..%c1%9c are further encodings of ../ and ..\.
  • A null byte, as in ?file=secret.doc%00.pdf, can make a Java application see .pdf while the operating system sees .doc. Attackers use this to bypass validation.

Filtering also backfires on its own terms. Remove ../ sequentially from .../...// and the leftover characters form ../ again. Filtering / alone is useless if the filesystem accepts \.

Fix one: stop accepting paths

The best control is to remove user input from the path. Use indexes rather than parts of file names. OWASP's example is a language file: the user submits 5 and the server maps that to the language, instead of trusting a returned name.

For downloads, store files under an ID and serve by ID, so the user never supplies a name or an extension. The template example becomes:

$templates = [1 => 'blue.php', 2 => 'red.php'];
$id = (int) ($_COOKIE['TEMPLATE'] ?? 1);
$template = $templates[$id] ?? 'blue.php';
include '/home/users/phpguru/templates/' . $template;

If you must accept a name, validate against known good values and reject the rest. Use a strict allowlist of characters, at most one . in the filename, no directory separators, and a list of permitted extensions. Reject bad input rather than trying to sanitise it.

Fix two: canonicalise, then check

Where input must reach a file API, decode it once, then resolve it with a built-in canonicalisation function before validating:

LanguageFunction
Crealpath()
JavagetCanonicalPath()
ASP.NETGetFullPath()
Perlrealpath() or abs_path()
PHPrealpath()

These remove .. sequences and symbolic links. After resolving, confirm the result still sits inside your base directory and refuse the request if it does not. A PHP sketch, with $name already past the allowlist:

$base = realpath('/home/users/phpguru/templates');
$full = realpath($base . '/' . $name);
if ($full === false || strpos($full, $base . DIRECTORY_SEPARATOR) !== 0) {
    http_response_code(404);
    exit;
}

Keep every check on the server. Client-side validation is trivially bypassed.

Limit the damage

Keep sensitive configuration files outside the web root. On Windows IIS, keep the web root off the system disk so traversal cannot reach system directories. Chrooted jails and code access policies restrict where files can be read or written, and they limit the damage when a check is missed.

For testing, replay the encodings above against every parameter you found, along with an absolute path and a remote URL. A penetration test will do this systematically.

Where to start

  1. List every file-handling call in your code, add the wrappers and framework helpers around them, and trace each path argument back to its source, including cookies and headers.
  2. Replace user-supplied file names with IDs mapped to files on the server, starting with downloads and template or language selection.
  3. For any remaining case, decode once, canonicalise with the platform function, and reject anything that resolves outside the base directory.
  4. Delete blocklist code that strips ../ or /, and replace it with an allowlist of characters, extensions and, where possible, values.
  5. Move sensitive configuration out of the web root, and stop returning error messages that reveal file paths.
  6. Test each parameter with the encoded, double-encoded, absolute and remote variants, and keep the cases as regression tests.

How we can help

Secure software

We build security into software from the first commit, and we build security tools of our own.

Contact us
AngelForce team

Written by the AngelForce team: security consultants with CEH, LPT Master and CISA certifications who test, monitor and defend systems for organisations across Europe.

Written with AI assistance from public sources listed below, and reviewed by the AngelForce team before publication.

Sources

  1. community.owasp.org/attacks/Path_Traversal
  2. cwe.mitre.org/data/definitions/22.html

Articles about Secure development

Tell us what you need to protect

Describe your environment and what you want to achieve. We will reply by email.

Contact us
info@angelforce.eu +372 5705 2044

AngelForce OÜ · Tallinn, Estonia