cybersecurity · intermediate · ~15 min · safe pentest lab
Learn to enforce secure password-at-rest storage by allow-listing modern adaptive-hash formats and treating everything else — plaintext, empty, NULL, or legacy/fast hashes — as a finding, using bounded string comparison that never over-reads attacker-controlled input.
A leaked user database is only survivable if the stored password values are adaptive hashes — bcrypt, argon2, or sha512-crypt — that force an attacker to spend enormous compute per guess. If a row instead holds the raw password ("plaintext") or a fast/legacy hash, one database dump instantly compromises every account, plus any account where the user reused that password elsewhere.
The insecure assumption you are auditing: "the value in the password column is fine because it's non-empty." It isn't. You must positively recognise a modern hash format and treat everything else as insecure.
Implement int looks_plaintext(const char *stored). Return 1 when stored looks like INSECURE plaintext, and 0 only when it begins with a recognised modern password-hash prefix:
$2a$, $2b$, $2y$ — bcrypt$argon2 — the argon2 family (argon2i / argon2d / argon2id)$6$ — sha512-cryptThis is a deny-by-default check: only the listed prefixes are trusted; anything else (including legacy $1$ md5-crypt or an unknown $2z$ variant) is a finding.
NULL and the empty string are both insecure, so they return 1. Bounds-check before reading: never index or scan past the terminating NUL of an attacker-controlled string.
Given the fixed harness array store, looks_plaintext(store[4]) where store[4] is a raw passphrase returns 1, while a $2b$-prefixed bcrypt row returns 0.
A single argument: const char *stored, a NUL-terminated string holding one stored password-column value (or NULL). It comes from a fixed in-harness array — never a live target, network, or filesystem.
An int: 1 if the value looks like insecure plaintext (no recognised modern hash prefix, or NULL/empty), otherwise 0.
C11, freestanding logic only (no I/O, no allocation needed). The harness provides <string.h>. Do not read past the NUL terminator; handle NULL and empty explicitly. Match prefixes case-sensitively.
int looks_plaintext(const char *stored){
/* TODO: inspect `stored` and return 1 only when it looks like insecure
plaintext (no recognised modern hash prefix). Remember NULL/empty are
insecure too, and bounds-check before reading. */
(void)stored;
return -1; /* stub: not implemented */
}Using strcmp/strlen on the pointer before checking it is non-NULL (crash on NULL). Merely checking that the string starts with '$' — legacy '$1$' md5-crypt and unknown '$2z$' variants also start with '$' but are insecure. Reading a fixed number of bytes with memcmp without accounting for a stored shorter than the prefix (over-read). Treating a non-empty value as automatically safe. Case-folding or trimming the prefix, which would accept malformed formats.
NULL pointer => 1. Empty string "" => 1. A value equal to exactly a prefix like "$2a$" with no hash body still counts as hash-formatted => 0. Legacy md5-crypt "$1$..." => 1 (not on the allow-list). Unknown bcrypt-like variant "$2z$..." => 1. A bcrypt-looking string missing its leading '$' => 1.
Solve this exercise in the browser editor — compile and run against the test harness, no setup required.