cybersecurity · intermediate · ~15 min · safe pentest lab
Perform canonicalization (case-folding, separator collapsing, trailing-separator stripping) with strict, write-time bounds checking so that a length- and case-varying input cannot overflow the output buffer or defeat downstream path matching.
When an API gateway routes a request, it first normalizes the raw path so that access-control rules match a single canonical form. If normalization is sloppy, an attacker can smuggle the same resource in under many spellings — /Admin, /admin//, /admin/ — and slip past an allow/deny list that only knows one of them. The asset here is a fixed request-path buffer inside the router; the threat is a caller-controlled string of arbitrary length and case.
The insecure assumption we are killing is "the input already fits and is already canonical." You must treat both the input length and its bytes as hostile.
Implement int normalize_path(const char *in, char *out, size_t cap). Write a normalized copy of in into out:
A–Z,/ into a single /,/ (but keep a bare root /).Bound-check before every write. If the normalized result (plus its NUL terminator) will not fit in cap, write nothing further and return -1. Otherwise NUL-terminate out and return its length.
Empty input, a lone /, runs like ///, a trailing slash that also needs collapsing (/a///), an exact-fit capacity, and a capacity of 0 all have defined behaviour.
in: a NUL-terminated C string (attacker-controlled, any length/case). out: caller buffer of cap bytes. cap: size of out in bytes.
Returns the length of the normalized string written to out (>= 0), or -1 if it would not fit in cap. out is always NUL-terminated on a non-negative return.
C11, freestanding logic (harness supplies includes). Never write at or beyond out[cap]. Reserve one byte for the NUL. cap == 0 must return -1. Only ASCII A-Z is lowercased. Do not read past the NUL of in.
int normalize_path(const char *in, char *out, size_t cap){
/* TODO: normalize `in` into `out` (lowercase, collapse '/', strip trailing '/').
This insecure stub copies blindly and ignores `cap` — replace it. */
(void)cap;
size_t n = 0;
while (in[n] != '\0') { out[n] = in[n]; n++; }
out[n] = '\0';
return (int)n;
}Checking capacity after writing instead of before. Forgetting to reserve a byte for the NUL (off-by-one). Stripping the trailing slash off the root so "/" becomes "". Collapsing slashes but then miscounting the length. Lowercasing non-ASCII or slash bytes. Using a signed char so bytes >= 0x80 sign-extend. Returning a length that does not match the NUL-terminated content.
Empty string -> "" length 0. Single/multiple leading-or-only slashes "///" -> "/" length 1 (root preserved). Trailing slash "/api/v1/" -> "/api/v1". Collapse + trailing "/a///" -> "/a". Exact fit: 7 chars into cap 8 succeeds; the same into cap 7 returns -1. cap == 0 returns -1 with no write.
Solve this exercise in the browser editor — compile and run against the test harness, no setup required.