Secure Coding in C · beginner · ~12 min
- Walk a NUL-terminated string one character at a time and classify each byte as uppercase, lowercase, digit, or special. - Track the four character-class flags and the length in a single pass, without reading past the terminator. - Represent a set of rules as a small configuration `struct` (a password policy) instead of hard-coding limits. - Report *every* failed requirement at once by returning a bitmask, where each bit stands for one rule. - Use the `<ctype.h>` classification functions safely (casting through `unsigned char`) and know when to avoid them. - Understand why a password policy is a cheap, high-value defensive layer, and where its limits are.
A password policy validator answers one narrow question: does this candidate password obey the rules we set? Rules are things like "at least 12 characters", "must contain an uppercase letter", "must contain a digit", and "must contain a special character". The validator does not store the password, hash it, or send it anywhere. It just reads the characters and decides pass or fail.
This builds directly on C strings. A password arrives as a const char * — a pointer to the first byte of a NUL-terminated array of characters. Everything you learned there applies: the string ends at the '\0' byte, you must never read past it, and you can walk it with a pointer or an index. Here we walk it once, and while walking we tally what we see.
In plain language: imagine reading a word letter by letter and ticking boxes on a checklist — "saw a capital letter", "saw a number", "saw a symbol". At the end you look at your checklist and the total length, compare them to what the policy demands, and hand back a report of anything missing. In C terminology, the checklist boxes are boolean flags, the report is a bitmask (one integer whose individual bits carry yes/no answers), and the character tests come from the standard <ctype.h> header.
This is the defensive counterpart to attacks like brute forcing. Instead of trying to crack passwords, you make weak passwords impossible to register in the first place.
A password policy is one of the cheapest and most effective defensive layers in any authentication system. A dozen lines of validation, enforced at sign-up and at every password change, quietly eliminate the weakest passwords — the password, 123456, and qwerty entries that dominate every leaked-credential dump and that automated brute-force and credential-stuffing tools try first.
The skill generalises far beyond passwords. "Read untrusted input once, classify each byte, and decide whether it is acceptable" is the shape of nearly all input validation: checking usernames, sanitising filenames, verifying that a field is numeric, rejecting control characters in a log line. Getting the string-walking and the pass/fail reporting right here is practice you will reuse constantly.
Returning a bitmask of every failure rather than a single yes/no also matters for real products. A good sign-up form tells the user "needs an uppercase letter and a digit" in one shot instead of making them guess one rule at a time. The bitmask is how the validation core communicates that full picture to the user interface.
Definition. The password is a const char * — the address of the first char in an array that ends with a '\0' (NUL) byte. const promises the function will not modify it.
Plain-language explanation. You do not get told the length up front. You discover the end by reading forward until you hit the '\0'. Every byte before it is a real password character.
How it works internally. In memory a password like "Ab3!" looks like this:
address: 0x100 0x101 0x102 0x103 0x104
value : 'A' 'b' '3' '!' '\0'
index : 0 1 2 3 4
pw ----> first byte; loop stops when *pw == '\0'
The five bytes are contiguous. pw[0] is 'A', pw[4] is the terminator. Reading pw[5] would be out of bounds.
When to use / not use. Walking with an index (for (size_t i = 0; pw[i]; i++)) is the natural tool here. Do not call strlen first and then loop a second time — that reads the whole string twice for no reason. One pass does everything.
Pitfall. Passing NULL instead of a real string. pw[0] on a NULL pointer is undefined behaviour and usually crashes. Check if (pw == NULL) at the top.
Knowledge check. For the string "pw", how many bytes are stored in memory, and what is the value of the last one?
<ctype.h>Definition. <ctype.h> provides isupper, islower, isdigit, isalnum, and friends. Each takes an int and returns non-zero (true) if the character belongs to that class.
Plain-language explanation. Instead of writing c >= 'A' && c <= 'Z' yourself, you call isupper(c). It reads better and handles the classes for you. A "special" character here means not a letter and not a digit — i.e. !isalnum(c).
How it works internally. These functions look the character up in a small table. The catch: their argument must be representable as an unsigned char or equal EOF. A plain char can be signed, so a byte like 0xE9 becomes a negative int, which is undefined behaviour if passed directly. The fix is to cast: isupper((unsigned char)c).
When to use / not use. Use <ctype.h> for readable, correct ASCII classification. Do not rely on it for full Unicode — it classifies one byte at a time and knows nothing about multibyte UTF-8 sequences.
Pitfall. isdigit(c) without the cast, on a string containing high-bit bytes, is a real, subtle bug. Always cast through unsigned char.
| Function | True when the character is... | Example true | Example false |
|---|---|---|---|
isupper(c) |
an uppercase letter | 'K' |
'k', '7' |
islower(c) |
a lowercase letter | 'k' |
'K', '!' |
isdigit(c) |
a decimal digit 0–9 |
'7' |
'x', ' ' |
isalnum(c) |
a letter or digit | 'k', '7' |
'!', ' ' |
!isalnum(c) |
a "special" (our definition) | '!', ' ' |
'k', '7' |
Knowledge check (predict the output). What does isalnum((unsigned char)'#') return, roughly, and is '#' a "special" character under our rule?
Definition. You keep four int flags — has_upper, has_lower, has_digit, has_special — all starting at 0, and set the relevant one to 1 the moment you see a matching character.
Plain-language explanation. Think of four light switches, all off. As you scan, the first uppercase letter flips has_upper on; it stays on. At the end, an off switch means "never saw one of those".
How it works internally. One for loop over the bytes. For each character you run a short chain of if tests and flip at most one flag. Length is just a counter you increment each iteration.
scan "aB3"
------------------------------------------
char has_upper has_lower has_digit has_special len
start 0 0 0 0 0
'a' 0 1 0 0 1
'B' 1 1 0 0 2
'3' 1 1 1 0 3
------------------------------------------
result: upper=yes lower=yes digit=yes special=no
When to use / not use. Perfect when the rules are independent "contains at least one of X" checks. Not the right tool if a rule needs counts (e.g. "at least two digits") — then use integer counters instead of 0/1 flags.
Pitfall. Resetting a flag inside the loop (has_upper = isupper(c)). That overwrites a previous 1 with 0 on the next non-uppercase character. Only ever set flags to 1; never back to 0 mid-scan.
Knowledge check (find the bug). A learner writes has_digit = isdigit((unsigned char)c) ? 1 : 0; inside the loop. Why does "a1b" end with has_digit == 0?
Definition. A pw_policy_t is a small struct holding the numbers and booleans that describe the rules: minimum length and which classes are required.
Plain-language explanation. Instead of baking 12 and "require a digit" into the code, you put them in data. Different apps can then enforce different policies with the same function.
How it works internally. The struct is passed by pointer (const pw_policy_t *p) so you read fields as p->min_length, p->require_digit, and so on. Passing by pointer avoids copying the struct and the const guarantees the validator will not alter it.
When to use / not use. Use a config struct whenever the same logic must serve multiple rule sets. For a single hard-wired policy in a tiny program, plain constants are fine — but the struct is the professional habit.
Pitfall. Forgetting that a require_* field of 0 means "not required". You must only fail a class check when the policy requires it: if (p->require_upper && !has_upper) fail....
Definition. A bitmask is a single integer used as a bag of yes/no flags. Each named bit stands for one requirement; a set bit means that requirement failed.
Plain-language explanation. Rather than return one boolean ("bad"), you return an integer where bit 0 might mean "too short", bit 1 "no uppercase", bit 2 "no lowercase", and so on. The caller inspects the bits to show precise messages. 0 means every bit clear — a clean pass.
How it works internally. You define one bit per rule with the left-shift operator, combine failures with bitwise OR (|=), and test with bitwise AND (&).
bit index : 4 3 2 1 0
meaning : special digit lower upper too-short
value : 1<<4 1<<3 1<<2 1<<1 1<<0
example: password too short AND missing a digit
fails = (1<<0) | (1<<3) = 0b01001 = 9
test "missing digit?": fails & (1<<3) -> non-zero -> yes
When to use / not use. Bitmasks shine when you have a small fixed set of independent flags and want them in one return value. Do not overuse them for large or dynamic sets — a struct of named booleans is clearer past a handful of flags.
Pitfall. Testing with == instead of &. fails == PW_NO_DIGIT is only true when the only failure is the digit; fails & PW_NO_DIGIT correctly detects the digit failure even when other bits are also set.
Knowledge check (explain in your own words). Why does returning a bitmask let a sign-up form list all password problems at once, while returning 1 for any failure cannot?
Named bit constants, the policy struct, and the core tests:
#include <ctype.h> // isupper, islower, isdigit, isalnum
#include <stddef.h> // size_t, NULL
/* One bit per rule. Left-shift makes each a distinct power of two. */
#define PW_TOO_SHORT (1u << 0) /* 0b00001 */
#define PW_NO_UPPER (1u << 1) /* 0b00010 */
#define PW_NO_LOWER (1u << 2) /* 0b00100 */
#define PW_NO_DIGIT (1u << 3) /* 0b01000 */
#define PW_NO_SPECIAL (1u << 4) /* 0b10000 */
typedef struct {
int min_length; /* e.g. 12 */
int require_upper; /* boolean: needs an uppercase letter? */
int require_lower; /* boolean: needs a lowercase letter? */
int require_digit; /* boolean: needs a digit? */
int require_special; /* boolean: needs a special char? */
} pw_policy_t;
/* classify one byte — always cast through unsigned char */
unsigned char c = (unsigned char)pw[i];
if (isupper(c)) has_upper = 1; /* set, never clear */
/* record a failure by OR-ing its bit in */
unsigned int fails = 0u;
if (p->require_digit && !has_digit) fails |= PW_NO_DIGIT;
/* the caller tests a single failure with AND */
if (fails & PW_NO_DIGIT) puts("add a digit");
Key points: each PW_* constant is a distinct power of two so they never collide; |= accumulates failures; & bit tests one; a return of 0 means "no bits set" = pass.
The defensive answer to "how do we stop brute-force?" is rarely "build a faster password cracker."
It is "enforce a sensible password policy at registration and password-change time."
This exercise teaches the validator side: given a candidate password and a policy, decide whether the password is acceptable.
No hashing. No cracking. No online tests. This is pure string and character-class logic.
The policy is a small struct describing the rules to enforce:
typedef struct {
int min_length; // e.g. 12
int require_upper; // boolean: needs an uppercase letter?
int require_lower; // boolean: needs a lowercase letter?
int require_digit; // boolean: needs a digit?
int require_special; // boolean: needs a special character?
} pw_policy_t;
Implement this function:
int pw_check(const char *pw, const pw_policy_t *p);
It should return:
0 — the password meets all required classes and the length requirement.For the non-zero case, each bit position represents which requirement failed. OR the failing bits together so the caller can see every problem at once.
strnlen(pw, MAX) if you want a hard cap on how far you read.'A'..'Z' and 'a'..'z' are locale-independent in C when you cast through unsigned char. That is fine here.1 for every failure. The test harness expects distinct bits, so the caller can tell which class is missing.#include <stdio.h> #include <ctype.h> #include <string.h> #include <stddef.h>
/* One bit per failed requirement. */ #define PW_TOO_SHORT (1u << 0) #define PW_NO_UPPER (1u << 1) #define PW_NO_LOWER (1u << 2) #define PW_NO_DIGIT (1u << 3) #define PW_NO_SPECIAL (1u << 4)
/* Hard cap so a missing terminator can never make us read forever. */ #define PW_MAX 256
typedef struct { int min_length; int require_upper; int require_lower; int require_digit; int require_special; } pw_policy_t;
/* Return 0 if pw satisfies p; otherwise a bitmask of failed rules. */ unsigned int pw_check(const char *pw, const pw_policy_t p) { if (pw == NULL || p == NULL) return PW_TOO_SHORT; / treat missing input as failing */
int has_upper = 0, has_lower = 0, has_digit = 0, has_special = 0;
/* strnlen never scans past PW_MAX bytes, even without a terminator. */
size_t len = strnlen(pw, PW_MAX);
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)pw[i]; /* safe for <ctype.h> */
if (isupper(c)) has_upper = 1;
else if (islower(c)) has_lower = 1;
else if (isdigit(c)) has_digit = 1;
else has_special = 1; /* not a letter/digit */
}
unsigned int fails = 0u;
if ((int)len < p->min_length) fails |= PW_TOO_SHORT;
if (p->require_upper && !has_upper) fails |= PW_NO_UPPER;
if (p->require_lower && !has_lower) fails |= PW_NO_LOWER;
if (p->require_digit && !has_digit) fails |= PW_NO_DIGIT;
if (p->require_special && !has_special) fails |= PW_NO_SPECIAL;
return fails;
}
/* Print a human-readable report for one password. */ static void report(const char *pw, const pw_policy_t *p) { unsigned int f = pw_check(pw, p); printf(""%s" -> mask %u : ", pw, f); if (f == 0) { printf("OK\n"); return; } if (f & PW_TOO_SHORT) printf("[too short] "); if (f & PW_NO_UPPER) printf("[no upper] "); if (f & PW_NO_LOWER) printf("[no lower] "); if (f & PW_NO_DIGIT) printf("[no digit] "); if (f & PW_NO_SPECIAL) printf("[no special] "); printf("\n"); }
int main(void) { pw_policy_t policy = { .min_length = 12, .require_upper = 1, .require_lower = 1, .require_digit = 1, .require_special = 1, };
report("password", &policy); /* too short, no upper/digit/special */
report("Sh0rt!", &policy); /* only too short */
report("CorrectHorse42!", &policy); /* passes every rule */
report("alllowercaseletters",&policy); /* long enough, missing classes */
return 0;
}
Setup. The PW_* macros give each rule a distinct bit. PW_MAX (256) is a safety ceiling on how far we ever read. pw_policy_t bundles the rules; main fills one with min length 12 and all four classes required, using designated initialisers (.min_length = 12).
Entering pw_check. First the guard: if pw or p is NULL, return a non-zero mask instead of dereferencing a null pointer. The four flags start at 0 ("not seen yet"). strnlen(pw, PW_MAX) gives the length but stops at 256 bytes even if the terminator is missing — a defensive substitute for strlen.
The scan loop. For each index i, we copy the byte into an unsigned char c — the cast that makes the <ctype.h> calls well-defined. The if / else if chain flips exactly one flag: uppercase → has_upper, lowercase → has_lower, digit → has_digit, anything else → has_special. Flags are only ever set to 1, so once true they stay true.
Trace for "Sh0rt!" (length 6, below the min of 12):
| i | pw[i] | class | flag set | flags after (U,L,D,S) |
|---|---|---|---|---|
| 0 | S |
upper | has_upper | 1,0,0,0 |
| 1 | h |
lower | has_lower | 1,1,0,0 |
| 2 | 0 |
digit | has_digit | 1,1,1,0 |
| 3 | r |
lower | (already) | 1,1,1,0 |
| 4 | t |
lower | (already) | 1,1,1,0 |
| 5 | ! |
special | has_special | 1,1,1,1 |
Building the mask. After the loop all four flags are 1, so no class bits are set. But len is 6 and p->min_length is 12, so (int)len < p->min_length is true and fails |= PW_TOO_SHORT sets bit 0. The function returns 1 (0b00001).
Reporting. report prints the raw mask then, using f & bit tests, the matching labels. For "Sh0rt!" only [too short] prints. For "CorrectHorse42!" every flag is set and the length is 15, so fails stays 0 and report prints OK. For "password" the mask is PW_TOO_SHORT | PW_NO_UPPER | PW_NO_DIGIT | PW_NO_SPECIAL = 1 | 2 | 8 | 16 = 27.
Expected output:
"password" -> mask 27 : [too short] [no upper] [no digit] [no special]
"Sh0rt!" -> mask 1 : [too short]
"CorrectHorse42!" -> mask 0 : OK
"alllowercaseletters" -> mask 26 : [no upper] [no digit] [no special]
Edge cases: the empty string "" gives length 0 and no flags, so it fails on PW_TOO_SHORT plus every required class. A password of exactly min_length passes the length test (the comparison is strictly <).
Mistake 1 — passing a plain char to isupper.
char c = pw[i];
if (isupper(c)) ... /* WRONG: char may be signed */
Why it is wrong: on platforms where char is signed, a byte ≥ 0x80 becomes a negative int, and passing a negative value (other than EOF) to isupper is undefined behaviour. Fix: cast through unsigned char.
unsigned char c = (unsigned char)pw[i];
if (isupper(c)) ... /* correct */
Recognise it: crashes or garbage classification only on inputs with high-bit bytes. Prevent it by always casting.
Mistake 2 — clearing a flag inside the loop.
has_digit = isdigit((unsigned char)pw[i]) ? 1 : 0; /* WRONG */
Why: this assigns every iteration, so a later non-digit resets a previously found 1 back to 0. "a1b" ends with has_digit == 0. Fix: only ever set to 1:
if (isdigit((unsigned char)pw[i])) has_digit = 1;
Mistake 3 — testing the mask with == instead of &.
if (fails == PW_NO_DIGIT) puts("add a digit"); /* WRONG when other bits set */
Why: == is only true when the digit failure is the only failure. If the password is also too short, the digit message never prints. Fix: if (fails & PW_NO_DIGIT).
Mistake 4 — collapsing all failures into a single value.
if (!has_digit) return 1;
if (!has_upper) return 1; /* WRONG: caller cannot tell which */
Why: returning 1 everywhere throws away which rule failed, so the UI can only say "invalid password". Fix: OR distinct bits and return the accumulated mask.
Mistake 5 — using < vs <= wrong for length. Deciding "too short" with len <= p->min_length rejects a password of exactly the minimum. The requirement is "at least min_length", so the failing condition is strictly len < p->min_length.
Compiler errors.
isupper/strnlen — you forgot #include <ctype.h> or #include <string.h>. (If strnlen is unavailable on your toolchain, compute the length with a bounded loop instead.)pw_policy_t undeclared — the typedef struct { ... } pw_policy_t; must appear before the function that uses it.len < p->min_length — cast: (int)len < p->min_length, or make min_length a size_t.Runtime errors.
pw was NULL or not terminated. Add the NULL guard and use strnlen(pw, PW_MAX).PW_MAX cap in strnlen prevents the runaway.Logic errors.
0 and are setting them to 1 (not the reverse).else if chain has a gap.<= vs <).Questions to ask when it misbehaves.
printf of has_upper..has_special and len right after the loop."", a 12-char all-lowercase string, and one that satisfies every rule. Do the masks match by hand?& (correct) or == (a common trap)?Reading past the buffer. The single greatest risk here is walking off the end of the string. strlen/an index loop assume a '\0' exists; a malformed buffer without one makes them read arbitrary memory (a buffer over-read — the class of bug behind Heartbleed). Defensive fix: bound every scan with strnlen(pw, PW_MAX) or an explicit i < PW_MAX guard, so you can never read more than a known number of bytes.
Null and uninitialised pointers. Dereferencing NULL or an uninitialised pw is undefined behaviour. Guard both pw and the policy pointer at the top and treat missing input as failing validation, never as passing.
Undefined behaviour in <ctype.h>. As covered above, passing a possibly-negative char to isupper/isdigit/isalnum is undefined. Always cast through unsigned char. This is a genuine, standard-mandated hazard, not a style preference.
Signed/unsigned length. strnlen returns size_t (unsigned). Comparing it to a signed min_length can warn or, in edge cases, mislead; cast deliberately so the comparison means what you intend.
Defensive security posture.
NULL, over-length, empty) return a failing mask, never a pass.Password123! (which passes every classic rule yet appears in breach lists). Modern guidance (NIST SP 800-63B) favours a generous length minimum plus a check against known-breached passwords over rigid composition rules. Enforce length here; pair it with a breach-list lookup in a real system. Everything in this lesson is local string logic — no hashing, no network, no live targets.Where this runs in real systems. Every sign-up and change-password endpoint runs a validator like this before accepting a new credential: web frameworks (the password rules in Django, Rails, ASP.NET Identity), operating systems (Linux PAM's pam_pwquality/pam_cracklib modules enforce length and class rules at passwd time), password managers giving strength feedback, and embedded devices forcing you off the default admin password. The same one-pass "classify each byte, decide pass/fail" pattern also drives username validation, filename sanitisation, and rejecting control characters in log output.
Best-practice habits.
Beginner:
PW_NO_DIGIT, has_upper), not magic numbers.unsigned char before <ctype.h> calls, every time.NULL inputs and fail closed.strnlen) so a bad string cannot run you off the buffer.min_length, all-one-class, and a fully valid password.Advanced:
Beginner 1 — Count the classes. Write void classify(const char *pw) that prints how many uppercase letters, lowercase letters, digits, and special characters a password contains. Requirements: one pass, cast through unsigned char, treat "special" as !isalnum. Example: input "Ab3!" prints upper=1 lower=1 digit=1 special=1. Concepts: string walking, <ctype.h>, counters.
Beginner 2 — Has a special character? Implement int has_special(const char *pw) returning 1 if pw contains at least one non-alphanumeric character, else 0. Requirements: return early once found; handle the empty string (returns 0) and NULL (returns 0). Example: "abc123" → 0, "abc 123" → 1. Concepts: single-pass scan, isalnum, early exit.
Intermediate 1 — Minimal pw_check. Implement unsigned int pw_check(const char *pw, const pw_policy_t *p) matching the lesson: length check plus the four class flags, returning a bitmask with the PW_* bit meanings. Requirements: NULL guard, bounded scan, only set flags to 1. Input/output: with min length 8 and all classes required, "aB3!aB3!" → 0; "aaaaaaaa" → PW_NO_UPPER | PW_NO_DIGIT | PW_NO_SPECIAL. Concepts: flags, bitmask, policy struct.
Intermediate 2 — Reject whitespace and report it. Extend pw_check with a new rule PW_HAS_SPACE that fails when the password contains any character where isspace is true. Requirements: add the bit without disturbing the existing ones, and update the reporter to print [has space]. Example: "Ab3! xyz9" sets PW_HAS_SPACE. Constraints: still one pass. Concepts: extending a bitmask, isspace.
Challenge — Count-based rules and a strength score. Change the policy so each class requirement is a minimum count (min_upper, min_digit, ...) instead of a boolean, and add a function int pw_strength(const char *pw, const pw_policy_t *p) returning a 0–100 score based on length and class variety (define your own reasonable formula). Requirements: keep the count logic in one pass, document your scoring, guard against integer overflow on absurdly long inputs (cap with PW_MAX), and print both the failure mask and the score. Hint: replace 0/1 flags with int counters and compare each against its minimum. Concepts: counters vs flags, policy design, defensive bounds, reporting.
A password policy validator reads a candidate password once, classifies each byte, and decides whether it obeys the rules — no hashing, no network, pure local string logic. It is one of the cheapest, highest-value defensive layers in authentication and a template for input validation in general.
Core ideas. Walk the NUL-terminated string a single time; for each byte, cast to unsigned char and use <ctype.h> (isupper, islower, isdigit, isalnum) to set one of four flags — only ever setting flags to 1, never clearing them. Track length as you go. Keep the rules in a pw_policy_t struct so one function serves many policies. Report results as a bitmask: one bit per rule, OR failures together with |=, test them with &, and let 0 mean a clean pass so the caller can list every problem at once.
Most important syntax. unsigned char c = (unsigned char)pw[i]; before any <ctype.h> call; #define PW_NO_DIGIT (1u << 3) for distinct bits; fails |= PW_NO_DIGIT; to record; if (fails & PW_NO_DIGIT) to test.
Common mistakes. Passing a signed char to isupper (undefined behaviour); clearing a flag mid-loop; testing the mask with == instead of &; collapsing all failures into a single 1; off-by-one on the length check; reading past a missing terminator.
Remember. Guard NULL, bound your scan with strnlen, fail closed, and know the limit: composition rules stop only the weakest passwords — real systems pair a generous length minimum with a breached-password check.