cybersecurity · intermediate · ~15 min · safe pentest lab

Normalize an API Request Path Safely

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.

Challenge

API Path Normalization

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.

Task

Implement int normalize_path(const char *in, char *out, size_t cap). Write a normalized copy of in into out:

  • lowercase every ASCII AZ,
  • collapse any run of / into a single /,
  • strip one trailing / (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.

Edge cases

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.

Input format

in: a NUL-terminated C string (attacker-controlled, any length/case). out: caller buffer of cap bytes. cap: size of out in bytes.

Output format

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.

Constraints

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.

Starter code

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

Common mistakes

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.

Edge cases to handle

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.

Background lessons

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