cybersecurity · advanced · ~15 min · safe pentest lab
Learn to defeat path traversal by canonicalizing an untrusted path (resolving '.'/'..'/absolute-root and empty segments) and then verifying the result stays inside a trusted base via an exact prefix check, while bounds-checking attacker-controlled lengths and counts before any buffer write.
A file-serving endpoint exposes a fixed document root (the base) and lets clients ask for a relative path beneath it. If you trust the raw path, an attacker sends ../../etc/passwd and reads secrets outside the served directory — the classic path traversal (CWE-22) vulnerability. The insecure assumption is that a string containing user input already refers to a safe location; it does not until you canonicalize it.
Implement int within_base(const char *base, const char *path): conceptually join path onto base, resolve every . and .. segment (and absolute paths, which reset to the filesystem root), then return 1 iff the resolved location still has base as an exact prefix, else 0. This is pure string logic over fixed inputs baked into the harness — no filesystem, network, or live target is touched.
Deny by default and bounds-check first: any attacker-controlled segment length or segment count must be validated before you copy it into a fixed buffer, and NULL inputs are rejected.
within_base(base, "static/app.js") -> 1within_base(base, "../secret.txt") -> 0within_base(base, ".") -> 1Edge cases: . and empty segments are no-ops, .. pops a segment (clamped at root), a path may dip above base and come back down (judge only the final resolved location), and over-long or over-numerous segments must be rejected rather than overflow.
Two C strings: base (a trusted, already-served document root such as "/var/www/html") and path (an untrusted, client-supplied relative or absolute path). Both may be NULL.
Return int 1 if the resolved join of path onto base keeps base as an exact path prefix (stays within base), otherwise return int 0.
No filesystem, network, or syscalls — pure string reasoning. Treat path as fully attacker-controlled. Individual segments are shorter than 64 bytes and a valid resolved path has fewer than 64 segments; anything exceeding those fixed bounds must be rejected (return 0), never allowed to overflow a buffer. NULL base or NULL path returns 0.
int within_base(const char *base, const char *path){
/* TODO: resolve '.'/'..' in path joined onto base and verify the result
stays within base. This insecure stub allows everything. */
(void)base; (void)path;
return 1;
}
Using strstr(path, "..") as the whole defense (misses absolute paths and rejects legitimate names like '..data'); decoding or checking the path once but resolving it differently later (TOCTOU-style mismatch); comparing base as a raw string prefix so '/var/www/html-evil' passes; forgetting that a leading '/' makes the path absolute and resets the root; writing each segment into a fixed buffer without checking its length or the segment count first, causing a stack overflow on hostile input; treating a temporary '..' rise as a permanent escape and rejecting valid 'sub/../file' paths.
Empty and '.' segments are no-ops; '..' pops one segment and clamps at the root rather than going negative; a path that climbs above base and then descends back into it is within base (judge the final resolved location, not intermediate state); an absolute path resets resolution to the root, so it is within base only if it re-derives the full base prefix; a segment >= 64 bytes or a path with >= 64 segments is rejected; NULL base or NULL path returns 0; the path '.' resolves to base itself (exact match, allowed).
Solve this exercise in the browser editor — compile and run against the test harness, no setup required.