cybersecurity · intermediate · ~15 min · safe pentest lab
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.
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.
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$0-9)$[./A-Za-z0-9]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.
valid_bcrypt(good_hash) -> 1 where good_hash is a full 60-char $2a$..$ stringvalid_bcrypt(short_tail) -> 0 where the base64 tail is shorter than 53 charsA 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.
An int: 1 if h is a structurally valid bcrypt hash, 0 otherwise.
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.
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;
}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.
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 '='.
Solve this exercise in the browser editor — compile and run against the test harness, no setup required.