cybersecurity · intermediate · ~15 min · safe pentest lab
Learn to neutralize Zip-Slip / directory-traversal by validating attacker-controlled archive entry names (rejecting absolute paths and `..` components) and bounds-checking the join against the destination buffer before writing.
When you extract an archive (zip, tar), every entry carries a name the attacker fully controls. If you blindly join the entry name onto your extraction directory, a malicious name like ../../etc/passwd or an absolute /etc/cron.d/evil will write outside the intended directory. This is the classic Zip-Slip / path-traversal vulnerability and it leads to arbitrary file overwrite and remote code execution.
The insecure assumption is "the entry name is a plain relative filename". It is attacker data. Never trust it.
Implement safe_join(dir, entry, out, cap) that combines the trusted base dir with the untrusted entry into out:
-1) any entry that is absolute (begins with /).entry containing a .. path component that would climb above dir (e.g. ../x, a/../b).cap, reject with -1 rather than overflow out.dir + / + entry into out, NUL-terminate it, and return the string length.A name that merely starts with .. such as ..foo is a legitimate filename, not traversal, and must be allowed. An empty entry resolves to the base directory itself. A dir that already ends in / must not produce a doubled separator.
safe_join is called with a trusted base directory string dir, an untrusted archive entry string entry, an output buffer out, and its capacity cap (in bytes, including room for the NUL).
Return the length of the joined path written to out on success (out is NUL-terminated). Return -1 and write nothing meaningful if the entry is absolute, contains a .. traversal component, or the result would not fit in cap.
C11, freestanding logic only. Do not call filesystem functions (no realpath/open/stat) — operate purely on the strings. Never write past out[cap-1]. Treat entry as fully attacker-controlled and deny by default. dir is trusted and non-NULL in normal use, but guard NULL pointers and cap==0 defensively.
int safe_join(const char *dir, const char *entry, char *out, size_t cap)
{
/* TODO: reject absolute paths and ".." traversal components, and
bounds-check the join against cap BEFORE writing. This naive stub
does none of that -- it trusts the entry and overflows out. */
(void)cap;
size_t n = 0;
while (dir[n]) { out[n] = dir[n]; n++; }
out[n++] = '/';
size_t k = 0;
while (entry[k]) { out[n++] = entry[k++]; }
out[n] = '\0';
return (int)n;
}Checking only for a leading ../ and missing .. deeper in the path (a/../../b); using strstr(entry, "..") which wrongly rejects legitimate names like ..foo or file..txt; forgetting to reject absolute paths, so the base dir is silently discarded; using strcat/sprintf with no capacity check and overflowing out; counting cap bytes but forgetting the NUL terminator needs one more.
Absolute entry (/etc/passwd) rejected; leading .. (../x) rejected; embedded .. component (a/../b) rejected; a filename that only starts with dots (..foo) allowed; empty entry yields just the base dir; dir already ending in / must not double the separator; exact-fit buffer succeeds while a buffer one byte short is rejected (not truncated); cap==0 and NULL out rejected without crashing.
Solve this exercise in the browser editor — compile and run against the test harness, no setup required.