AppSec · Jul 7, 2026 · 7 min read

Path traversal: confirming a read without touching /etc/passwd

A traversal probe that returns /etc/passwd runs a real exploit on a production host. A benign nonce oracle confirms the same sink without reading any sensitive file.

Path traversal has one of the clearest confirming signals in DAST - if ../../../etc/passwd returns the root-account block, the sink is proven. The problem is that confirming it that way is itself an exploit: you read a real file from a production host, your scanner logs contain system content it should never hold, and the whole approach breaks against hardened or minimal containers where /etc/passwd does not exist. A benign nonce oracle confirms the same fact without any of those costs.

01Reachability is not a confirmed read

The first signal of a path traversal gap is an error that changes with the traversal sequence. A 'file not found' response that varies as you add ../ segments is evidence the application is interpreting the path and trying to open it rather than blocking it outright. That is reachability: the sink processes user-supplied filenames. It is worth reporting as a reachable finding, but it is not a confirmed arbitrary read.

Confirmed requires observing content that could only arrive via a boundary crossing. Most tools confirm by requesting /etc/passwd and matching on the root: line. That works on uncontainerized Linux - and fails silently on every other platform, reads a real file from a real production host, and puts system content into scan logs. The nonce oracle avoids all three failure modes.

Standard probe ──► GET /docs?file=../../../etc/passwd Response ──► root:x:0:0:root:/root:/bin/bash (real system file read) Nonce probe ──► GET /docs?file=../../uploads/apnonce-62615533.txt (nonce file written by probe before the request) Response ──► 62615533 (only a traversal read returns this value)
Left: proves the bug, reads a real file. Right: proves the same bug with a benign nonce the probe wrote and controls.

02The benign nonce oracle in practice

Before probing, write a temporary file containing only the nonce string 62615533 (7919 x 7907 - uniquely attributable to this probe run) to a directory the application is known to be able to write to: an upload path, a temp directory visible in access logs, or a public static-asset path identified from earlier crawl output. Then send the traversal payload targeting that specific file from the parameter under test. If the response body contains the nonce, the sink is confirmed: the application read across the directory boundary and returned the content. Delete the nonce file immediately after the probe - even if the probe times out.

path-traversal-probe.pypython
import requests, pathlib

NONCE = "62615533"  # 7919 * 7907
UPLOAD_DIR = pathlib.Path("/var/www/uploads")

def confirm_traversal(base_url: str, param: str) -> bool:
    nonce_file = UPLOAD_DIR / "apnonce-62615533.txt"
    nonce_file.write_text(NONCE)
    try:
        # probe traverses from the doc-serve root up to uploads/
        payload = "../../uploads/apnonce-62615533.txt"
        resp = requests.get(base_url, params={param: payload}, timeout=8)
        return NONCE in resp.text
    finally:
        nonce_file.unlink(missing_ok=True)  # always clean up

The nonce file contains only the arithmetic marker - no credentials, no system content. If cleanup fails, the worst outcome is a short-lived file in a writable directory, not a backdoor. Every read in the probe is against a file the probe itself put there.

Proven - HighPath traversal read - /api/docs?file= parameter
GET /api/docs?file=../../uploads/apnonce-62615533.txt ──► 200 OK body contains: 62615533 (nonce recovered via traversal) baseline: GET /api/docs?file=apnonce-62615533.txt ──► 404 Not Found differential holds across 3 repeats; no response variation
Oracle: benign nonce read. No system file accessed during probe. Severity high - confirmed arbitrary read below the web root. Escalation path: assess reachable config files (WEB-INF/web.xml, .env, config/database.yml) with read-only nonce check before deciding critical.

03Platform and encoding matrix

Linux and Windows normalize traversal sequences differently, and a probe tuned for one silently fails against the other. Linux resolves ../ and uses / exclusively. Windows accepts both / and \ and resolves ../ via the Win32 path API, so ../ still works on Windows even though the native separator is \. URL encoding adds another layer: ..%2f and ..%5c are decoded by some frameworks before the filesystem call and left encoded by others. Double encoding (%252f) reaches applications sitting behind a URL-decoding reverse proxy that normalizes once on the way in.

Platform / runtimeSeparatorsKey encoding variantsCommon bypass
Linux/..%2f, ..%252fDouble-URL-encode through decoding proxy
Windows (IIS / Win32)/ and \..%5c, ..\/Mixed-separator sequence bypasses naive ../ filter
Java (Path.of)/ or OS default%2e%2e%2fRaw string concat bypasses Path.normalize() if applied before API call
PHP (include / fopen)/ or \..%2f, %00 (null byte)Pre-5.3 null-byte truncation still present on unpatched installs

The engine fingerprints the tech stack first and selects the probe set accordingly. A Java application behind a URL-decoding Nginx proxy receives a double-decoded path; the probe must send %252e%252e%252f to land the right byte sequence after two passes. Probing per observed fingerprint is the only way to avoid a false negative on every variant a single traverse string does not cover.

04Severity follows what the read path can actually reach

A confirmed traversal rated at a flat 'high' loses the severity dimension that drives the remediation SLA. Reading across to a sibling upload directory is one impact profile; reading the configuration file that contains a database password is another. Two facts decide severity: the traversal is confirmed via the nonce, and the escalation target is assessed - what sensitive files sit above the web root on this specific host?

The escalation assessment uses read-only probes against predictable paths for the detected framework: WEB-INF/web.xml for Java, config/database.yml for Rails, .env for Node, application.properties for Spring Boot. Each is probed by replacing the nonce write step with a known-string check - the real file content is never retrieved unless the operator explicitly accepts the consent boundary. If the known-string check (a line known to appear in that file type) returns positive, the severity escalates to critical with a 7-day SLA. If not, it stays at high with a 30-day SLA and the confirmed nonce path as the artifact.

0
system files read during probe
4
platform and encoding variants probed per target
1
nonce - same arithmetic oracle, cleaned up after each run

The result is a finding a developer can act on and an auditor can verify: confirmed traversal, bounded blast radius, no sensitive data in the scan log, severity tied to what the confirmed read path can demonstrably reach - not to what a vulnerability category label implies.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →