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.
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.
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.
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.
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 / runtime | Separators | Key encoding variants | Common bypass |
|---|---|---|---|
| Linux | / | ..%2f, ..%252f | Double-URL-encode through decoding proxy |
| Windows (IIS / Win32) | / and \ | ..%5c, ..\/ | Mixed-separator sequence bypasses naive ../ filter |
| Java (Path.of) | / or OS default | %2e%2e%2f | Raw 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.
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.
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.