Path traversal (directory traversal): how ../ works, how testers prove it, how to fix it

Path traversal, also called directory traversal, lets an attacker read or write files outside the folder an application meant to expose, by putting sequences such as ../ in a file name or path. Fix it by resolving the full path and checking that it stays inside a base directory, or by mapping IDs to server-side paths. Testers prove it by reading a harmless known file.

Path traversal, also called directory traversal, is a flaw that lets an attacker read or write files outside the folder an application meant to expose. The attacker puts path elements such as ../ into a file name or path, and the application joins them to a base folder without checking where the result lands. CWE-22 describes it as using external input to build a pathname meant to sit "underneath a restricted parent directory" without neutralizing special elements that can make it resolve elsewhere.

Testers check for path traversal in web application penetration testing and API penetration testing. Testing needs written authorization from the system owner.

What is path traversal, and is directory traversal the same thing?#

Path traversal and directory traversal are two names for one flaw. CWE-22 prefers "path traversal" and lists "directory traversal" as an alternate term. OWASP also calls it dot-dot-slash. The flaw lets a user step up out of an intended folder.

The result depends on what the application does with the path:

  • Read: the attacker downloads a file the app never meant to serve, such as a configuration file.
  • Write: the attacker saves or replaces a file outside the upload folder.
  • Include: the app loads the file as a template or script, which is local file inclusion (LFI).
  • Delete: a delete feature removes a file the user does not own.

How does a path traversal attack work?#

A path traversal attack works because the operating system resolves .. as "the parent folder." If the application appends user input to a base folder and then opens the result, enough .. elements move the final path out of that folder.

Vulnerable download endpoint (fictional app)

This is a demonstration. FileShelf is a fictional app on app.example.com, and no real customer system was tested.

The app lets a user download an invoice by file name.

# Vulnerable: the name is joined to the folder with no check
@app.get("/download")
def download():
    name = request.args["file"]
    return send_file("/srv/fileshelf/invoices/" + name)

Normal request:

GET /download?file=invoice-2026-10.pdf HTTP/1.1
Host: app.example.com

A tester with authorization plants a marker file marker.txt in the parent folder of the invoices folder, then asks for it:

GET /download?file=../marker.txt HTTP/1.1
Host: app.example.com

If the response body contains the unique marker string, the app served a file outside the invoices folder. A control request for ../does-not-exist.txt must return a different error. The pair shows that the traversal, not a generic error page, produced the result.

Which encodings and variants appear in path traversal tests?#

Applications often filter the plain ../ and then decode the input later, so testers check the encoded and nested forms that OWASP documents. This table lists the families and the weak filter each one exposes. It does not list working strings for real products.

Variant What it is Filter weakness it exposes Fix
../ and ..\ The plain forms. Windows accepts both slashes Filter checks only one slash type Resolve the path, then check containment
URL encoding (%2e%2e%2f) The same characters, percent-encoded Check runs before decoding Decode once, then validate
Double encoding Encoded twice, so a second decode produces ../ A layer decodes after the check Validate after the last decode
Overlong UTF-8 forms Non-standard encodings of / that some old decoders accept Decoder differs from the filter Reject non-canonical input
Nested sequences A sequence that becomes ../ after a strip One-pass stripping of ../ Never strip. Resolve and check
Absolute path A full path such as /etc/hostname in place of a name Code that only blocks .. Reject absolute paths, or join safely
Null byte A terminator that cuts off an appended extension Old runtimes that pass the string to C APIs Reject control characters
Windows forms Drive letters and UNC paths (\\host\share) Code that only handles / Use the platform's path library

The OWASP page on path traversal lists these encodings. The rule that covers all of them is the same: do not try to recognize bad input. Resolve the final path and compare it with the base folder.

Where does path traversal hide?#

Path traversal hides in any code path that turns a request value into a file path. Download endpoints are the obvious case. The less obvious ones are the ones testers find.

Place Example parameter or feature Risk
File download or preview file, name, path Read
Template or language selection lang, theme, page Read or include (LFI)
Image resize and thumbnail services src, image Read
Static file routes URL path under /static/ Read
Upload file names filename in multipart data Write. See file upload vulnerabilities
Archive extraction Entry names inside a zip or tar Write (Zip Slip)
Log, export and backup viewers log, report Read

Zip Slip deserves its own note. Snyk published the Zip Slip research on 5 June 2018 and described it as an arbitrary file overwrite flaw through archive entries whose names contain ../. The flaw sits in the extraction code, so it appears even when no URL parameter is involved.

How does path traversal differ from LFI and file upload flaws?#

Path traversal chooses a file path. LFI makes the app load that file as code. A file upload flaw puts attacker-chosen content on the server. They often appear together, but each needs a different fix.

Path traversal Local file inclusion File upload flaw
What the attacker controls A file path A file the app loads as code or template File content, name and type
Typical result Read or write a file Code or template runs from a local file Stored file runs, or is served as a script
Core fix Resolve and check containment Never build include paths from input Validate, rename and store safely
CWE CWE-22 CWE-98 CWE-434

How do you fix path traversal in code?#

Fix path traversal in one of two ways. The best way is to never use the user's value as a path: map an ID to a stored path. When you must use a file name, resolve the full path and check that it is still inside the base directory.

The OWASP and PortSwigger guidance agrees on this order: avoid passing user input to file APIs, and when you cannot, validate the input and then check the canonical path begins with the expected base folder. These examples show the pattern in five stacks.

Python

from pathlib import Path

BASE = Path("/srv/fileshelf/invoices").resolve()

def open_invoice(name: str):
    target = (BASE / name).resolve()
    if not target.is_relative_to(BASE):
        raise PermissionError("outside base directory")
    return target.open("rb")

Flask's send_from_directory already does this check. Its documentation says it "uses safe_join() to ensure the path coming from the client is not maliciously crafted to point outside the specified directory."

Node.js

const path = require("node:path");
const fs = require("node:fs/promises");

const BASE = path.resolve("/srv/fileshelf/invoices");

async function readInvoice(name) {
  const target = path.resolve(BASE, name);
  if (!target.startsWith(BASE + path.sep)) throw new Error("outside base directory");
  const real = await fs.realpath(target);
  if (!real.startsWith(BASE + path.sep)) throw new Error("symlink escape");
  return fs.readFile(real);
}

Java

Path base = Paths.get("/srv/fileshelf/invoices").toRealPath();
Path target = base.resolve(name).normalize();
if (!target.startsWith(base)) {
    throw new SecurityException("outside base directory");
}
byte[] data = Files.readAllBytes(target.toRealPath());

Go

// Go 1.24 or later: os.Root refuses paths that leave the directory,
// including symlinks that point outside it.
root, err := os.OpenRoot("/srv/fileshelf/invoices")
if err != nil { return err }
defer root.Close()
f, err := root.Open(name)

The Go team's post on os.Root presents it as a simple defense against unintentional path traversal. On older Go versions, use filepath.IsLocal(name) for names that must stay relative.

PHP

$base = realpath('/srv/fileshelf/invoices');
$target = realpath($base . DIRECTORY_SEPARATOR . $name);
if ($target === false || strpos($target, $base . DIRECTORY_SEPARATOR) !== 0) {
    http_response_code(403);
    exit;
}
readfile($target);

Two cautions apply to every stack. Compare against the base path plus a separator, because /srv/fileshelf/invoices-old also starts with /srv/fileshelf/invoices. And check symlinks, because a link inside the folder can point out of it.

How do you stop Zip Slip when extracting archives?#

Check every entry's final path before writing it. Do not trust the name inside the archive. In Python 3.12 and later, the tarfile module has extraction filters. The documentation says that since Python 3.14 the default filter is data, which blocks the most dangerous issues but not every unintended or insecure behavior. For older versions, pass the filter explicitly:

with tarfile.open("upload.tar") as tar:
    tar.extractall("/srv/fileshelf/work", filter="data")

For zip files in Java, resolve each entry against the destination folder, normalize it, and reject any entry whose path does not start with the destination.

Which framework defaults help, and which calls break them?#

Frameworks help most on static file routes and least where your code builds the path itself. Flask's send_from_directory applies safe_join, as its documentation states. Go 1.24's os.Root blocks escapes at the API level. Calls that take a full path (send_file in Flask, File and Paths.get in Java, fs.readFile in Node.js, readfile in PHP) apply no check at all, so each one needs the containment check above. Read your framework's documentation for the exact behavior of its static file handler and test it.

How do penetration testers detect and prove path traversal?#

Testers prove path traversal with a differential response: a request that returns a file the app should not serve, beside controls that return something different. A 200 status is not proof, because many apps return 200 with an error page.

Our method follows OWASP WSTG-ATHZ-01. We use test-owned accounts and records, save each request and response with a timestamp, and label results as Observed.

  1. Request a legitimate file and save the response as the baseline.
  2. Request a planted marker file one level up. A unique string in the body is the proof.
  3. Request a path that does not exist, and compare the error with the marker result.
  4. Request the same path anonymously and signed in, to see whether access control also differs.
  5. Try the encoded forms from the table above, one at a time, and record which layer decodes.
  6. Where the scope allows, read one harmless system file, such as the host name file, and stop there.

A proof that reads real credentials or private user data is not needed and is not performed. Write tests use a test-owned file, marked as test data, and the file is removed afterward. The report records the file read, the path used and what was left untouched. If a control shows the first result was misleading, we downgrade or drop the finding.

Sample finding (fictional)

This is a demonstration. No real customer system was tested.

Finding: path traversal in GET /download on app.example.com. Severity: High. A request for ../marker.txt returned the planted marker string FS-MARKER-7731. A control for ../missing.txt returned a 404 error page. A signed-out request to the same path also succeeded, so the endpoint has no access check. Fix: resolve the path, check it is inside the invoices folder, and require a session.

Which real vulnerabilities were path traversal?#

Path traversal flaws have appeared in web servers and network appliances, and NVD records them under CWE-22. The dates below are NVD publication dates.

Date CVE What NVD says
8 May 2019 CVE-2019-11510, Pulse Connect Secure An unauthenticated remote attacker can send a specially crafted URI to perform an arbitrary file reading vulnerability (CWE-22)
4 June 2019 CVE-2018-13379, Fortinet FortiOS SSL VPN web portal An unauthenticated attacker can download system files with specially crafted HTTP requests (CWE-22)
5 October 2021 CVE-2021-41773, Apache HTTP Server 2.4.49 A change to path normalization lets an attacker map URLs to files outside the directories set by Alias-like directives (CWE-22)
7 October 2021 CVE-2021-42013, Apache HTTP Server 2.4.50 NVD says the fix for CVE-2021-41773 in 2.4.50 was insufficient (CWE-22)

The Apache case shows why a path-normalization change needs tests: a fix for one encoding left another working.

Path traversal testing checklist#

  1. List every parameter, header and route that becomes a file path, template name or archive entry.
  2. Confirm scope and written authorization, and agree which files may be read.
  3. Plant a marker file and record its path.
  4. Capture a baseline response, a not-found response and a marker response.
  5. Test plain, encoded, nested and absolute forms, one at a time.
  6. Test Windows separators and drive letters where the host runs Windows.
  7. Test upload file names and archive entry names for write access.
  8. Compare anonymous and signed-in access to the same path.
  9. Check that the app does not follow symlinks out of the base folder.
  10. Retest after the fix with the same requests.

When should you get a professional test?#

Get a professional test when your application serves files by name, resizes or converts user files, extracts archives, or loads templates by a request value. Scanners find the simple file= cases. They miss second-order cases such as a stored file name used later by a batch job. Excession Labs includes path traversal checks in web application penetration testing and API penetration testing, under written authorization.

Related guides: file upload vulnerabilities, server-side request forgery and OS command injection.

Questions

Is directory traversal the same as path traversal?

Yes. Directory traversal, path traversal, dot-dot-slash and backtracking are names for the same flaw. CWE-22 covers all of them. Search results use both names, so this page uses both.

What is the difference between path traversal and LFI?

Path traversal reads or writes a file at a path the attacker controls. Local file inclusion (LFI) makes the application load a local file as code or template, so the file can run. LFI usually needs a traversal flaw to choose the file, but traversal alone does not run anything.

Does removing ../ from the input fix path traversal?

No. Stripping the sequence once can leave a working sequence behind, and encoded forms may be decoded after the check. Resolve the full path first, then check that it sits inside the allowed base directory.

Can path traversal write files, alone read them?

Yes. If a path is used to save an upload or extract an archive, the attacker may overwrite files outside the target folder. Zip Slip is the archive version of this flaw.

How do I test for path traversal without reading real secrets?

Plant a marker file with a unique string in a known folder, then try to read it through the vulnerable parameter. Compare the response with a control request for a file that does not exist. Read a harmless system file only when the scope allows it.

Sources

  1. CWE-22: Improper Limitation of a Pathname to a Restricted Directory (Path Traversal) (checked 9 November 2026)
  2. OWASP Path Traversal (checked 9 November 2026)
  3. OWASP WSTG-ATHZ-01: Testing Directory Traversal File Include (checked 9 November 2026)
  4. PortSwigger Web Security Academy: Path traversal (checked 9 November 2026)
  5. Snyk: Zip Slip vulnerability (checked 9 November 2026)
  6. Flask API: send_from_directory (checked 9 November 2026)
  7. Python documentation: tarfile extraction filters (checked 9 November 2026)
  8. Go blog: Traversal-resistant file APIs (os.Root) (checked 9 November 2026)
  9. NVD: CVE-2021-41773 (checked 9 November 2026)
  10. NVD: CVE-2021-42013 (checked 9 November 2026)
  11. NVD: CVE-2018-13379 (checked 9 November 2026)
  12. NVD: CVE-2019-11510 (checked 9 November 2026)