cybersecurity · intermediate · ~15 min · safe pentest lab

Zip-Slip Safe Path Join

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.

Challenge

The threat

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.

Your task

Implement safe_join(dir, entry, out, cap) that combines the trusted base dir with the untrusted entry into out:

  • Reject (return -1) any entry that is absolute (begins with /).
  • Reject any entry containing a .. path component that would climb above dir (e.g. ../x, a/../b).
  • Be bounds-safe: if the result plus its NUL terminator would not fit in cap, reject with -1 rather than overflow out.
  • On success, write dir + / + entry into out, NUL-terminate it, and return the string length.

Edge cases

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.

Input format

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).

Output format

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.

Constraints

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.

Starter code

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;
}

Common mistakes

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.

Edge cases to handle

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.

Background lessons

Solve this exercise in the browser editor — compile and run against the test harness, no setup required.