cybersecurity · intermediate · ~15 min · safe pentest lab

Validate a bcrypt Hash Structure

Practice deny-by-default input validation: verify a fixed, well-known string format field-by-field, bounds-checking each position before you read it and rejecting anything that does not match exactly, rather than accepting-then-patching.

Challenge

The problem

When your login backend loads a stored password hash from the user database, it must not blindly trust that the string is a well-formed bcrypt hash. A corrupted, truncated, or attacker-planted value (for example a row edited through a separate SQL-injection bug) can trick a naive verifier into odd behaviour: some crypt implementations silently return a truncated or empty result for a malformed salt, which can make an any-password-matches condition possible. Rejecting malformed hashes before they ever reach crypt()/bcrypt_checkpw() is a cheap, defensive guard.

The asset here is the stored hash in a fixed in-memory buffer; the threat is a malformed or malicious hash string. Your job is a pure structural check with deny-by-default semantics.

The task

Implement:

int valid_bcrypt(const char *h);

Return 1 iff h has the exact modular-crypt bcrypt structure, otherwise 0:

  • $2 followed by exactly one of a, b, or y
  • a $
  • exactly two cost digits (0-9)
  • a $
  • exactly 53 characters from the bcrypt base64 alphabet [./A-Za-z0-9]
  • then the terminating NUL — nothing more, nothing less.

Walk the string one byte at a time and stop the moment a byte is wrong; never read past the NUL, and treat a NULL pointer as invalid. This operates only on the fixed buffer the harness hands you — never a live target, socket, or file.

Example

  • valid_bcrypt(good_hash) -> 1 where good_hash is a full 60-char $2a$..$ string
  • valid_bcrypt(short_tail) -> 0 where the base64 tail is shorter than 53 chars

Input format

A single C string h (const char *). It may be NULL, empty, a valid 60-character bcrypt hash, or any malformed variant. It is always NUL-terminated when non-NULL.

Output format

An int: 1 if h is a structurally valid bcrypt hash, 0 otherwise.

Constraints

C11, no dynamic allocation. Do not read past the terminating NUL. Handle NULL by returning 0. The valid base64 alphabet is exactly [./A-Za-z0-9] (note: '+' and '=' are NOT part of it). The tail after the second cost '$' must be exactly 53 characters. Total length of a valid hash is 60 bytes.

Starter code

int valid_bcrypt(const char *h){
    /* TODO: implement structural validation of a bcrypt hash.
       Required structure:
         '$2' + one of {a,b,y} + '$' + two cost digits + '$'
         + exactly 53 chars from [./A-Za-z0-9] + NUL.
       Deny by default; bounds-check every byte before reading it;
       treat NULL as invalid. Replace this insecure stub. */
    (void)h;
    return -1;
}

Common mistakes

Using strlen and index arithmetic without first confirming each prefix byte, so a short string is read out of bounds; accepting the hash if it merely starts with '$2'; forgetting to reject strings LONGER than 60 bytes (must confirm the byte after the 53-char tail is NUL); including '+' or '=' in the accepted alphabet (that is standard base64, not bcrypt's variant); not handling NULL.

Edge cases to handle

NULL pointer; empty string; a lone "$"; correct prefix but wrong version letter (e.g. $2x); non-digit cost characters; a tail shorter than 53 chars (early NUL); a tail longer than 53 chars (trailing junk after a valid 53); a tail of exactly 53 chars but containing a byte outside [./A-Za-z0-9] such as '+' or '='.

Background lessons

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