cybersecurity · beginner · ~15 min · safe pentest lab

Validate a JWT Segment Charset (base64url)

Enforce a strict allowlist (deny-by-default) charset check on attacker-controlled bytes, using explicit length rather than NUL-termination, so an API gateway rejects malformed JWT segments before any decode step.

Challenge

The problem

A JWT arrives as three dot-separated segments (header.payload.signature). Each segment MUST be base64url encoded, meaning every byte comes from the charset [A-Za-z0-9_-] — note the URL-safe - and _ instead of + and /, and no = padding.

An API gateway that decodes a segment without first validating its charset is trusting attacker-controlled bytes. Sneaking in +, /, =, a . delimiter, an embedded \0, or high-bit bytes can confuse a permissive decoder, smuggle data past a WAF, or trigger parser differentials between your gateway and your auth service.

Your task

Implement a strict validator that operates only on a fixed in-harness buffer:

int is_base64url(const char *seg, size_t n);

Return 1 iff every one of the n bytes of seg is in [A-Za-z0-9_-], otherwise 0. Deny by default: n == 0 (empty segment) returns 0, and a NULL pointer returns 0.

The check must be exact — inspect all n bytes, never stop at a \0, and reject anything outside the allowed set.

Example

Given header = "eyJhbGciOiJIUzI1NiJ9" (a real base64url header, 20 bytes):

  • is_base64url(header, 20) -> 1
  • is_base64url(header, 0) -> 0

Input format

A byte pointer seg and a length n (size_t). Bytes are raw and may include any value 0-255, including embedded NUL. Only the first n bytes are in scope.

Output format

int: 1 if all n bytes are base64url characters [A-Za-z0-9_-]; otherwise 0.

Constraints

C11, freestanding logic (no I/O, no allocation). Treat bytes as unsigned when range-checking. Do not read past index n-1. Do not stop early at a NUL byte. n can be 0. seg can be NULL.

Starter code

int is_base64url(const char *seg, size_t n){
    /* TODO: verify every one of the n bytes of seg is in the
       base64url charset [A-Za-z0-9_-]. Reject '+','/','=', '.',
       embedded NUL, and high-bit bytes. Deny by default:
       n==0 -> 0, NULL -> 0. Treat bytes as UNSIGNED.

       This insecure stub trusts the input and always allows it. */
    (void)seg; (void)n;
    return 1;
}

Common mistakes

Using strlen or stopping at the first '\0' instead of honoring n; forgetting to reject '+' and '/' (accepting standard base64 instead of base64url); allowing '=' padding; comparing chars as signed so bytes >=0x80 sneak through as negative; forgetting the n==0 and NULL deny-by-default cases; using isalnum() which is locale-dependent and does not include '-' or '_'.

Edge cases to handle

n==0 returns 0; NULL pointer returns 0; embedded '\0' inside the n bytes must be rejected (not treated as end-of-string); '+', '/', and '=' from standard base64 must be rejected; the '.' JWT delimiter must be rejected; high-bit / non-ASCII bytes (>=0x80) must be rejected; a single valid char returns 1.

Background lessons

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