Case-Sensitivity
Two Systems, Same String, Different Answer
A user registers with username Admin. Later they try to register again with admin. The system rejects it: "Username already taken." Good — the system normalizes usernames to lowercase and prevents collisions.
Three months later, a security engineer finds that ADMIN and Admin return the same profile page, but the password check for Admin only matches when you submit the original case. The normalization happens inconsistently: at registration it lowercases, at login it does not.
Case-sensitivity bugs are rarely about the case itself. They are about inconsistent application of a policy across different code paths.
The Two Fundamental Questions
Before testing any text field for case sensitivity, answer two questions:
-
Is this field case-sensitive by design? Passwords yes. Usernames — depends on the application. Email addresses — per RFC, the local part is technically case-sensitive, but virtually all mail systems treat them as case-insensitive. Enum values and status codes — usually case-sensitive.
-
Is the policy applied consistently? If usernames are case-insensitive, does that apply equally to registration, login, password reset, profile lookup, admin search, API endpoints, and error messages? Every code path that touches the field is a separate opportunity for the policy to be violated.
Database-Level Case Sensitivity
The database is often the source of truth for case sensitivity, and it has its own rules:
PostgreSQL: String comparisons are case-sensitive by default. WHERE username = 'Admin' does NOT match admin. To get case-insensitive comparison, you must use ILIKE, LOWER(username) = LOWER(?), or a citext column type.
MySQL: The default collation is utf8mb4_general_ci (case-insensitive). WHERE username = 'Admin' matches admin, ADMIN, and admin. Switching to a _cs (case-sensitive) collation changes this.
SQLite: LIKE is case-insensitive for ASCII characters only. For Unicode characters, it is case-sensitive. WHERE username LIKE 'admin' matches ADMIN but does NOT match АDMIN (where the A is Cyrillic).
This means the same SQL query behaves differently across databases. A test that passes on SQLite (which the team uses for local testing) may fail on PostgreSQL (production).
Case Sensitivity in Security
Case sensitivity is not just a data integrity question — it is a security question in several specific scenarios:
Token validation: Password reset tokens, email verification links, and session tokens should be case-sensitive. A case-insensitive comparison doubles (or more) the chance of a brute-force match succeeding by allowing partial matches.
Permission checks: A role check that compares userRole.equals("ADMIN") case-sensitively will fail silently if the database stores admin. The user gets no access and no error — they simply cannot perform admin actions. No one files a bug because they assume it is working as designed.
Username enumeration: If the login endpoint normalizes case but the "forgot password" endpoint does not, an attacker can probe whether Admin is a registered user by submitting to forgot-password and observing whether the response says "email not found" (case-sensitive miss) vs. "check your inbox" (case-insensitive hit in login flow).
Testing for Case Sensitivity
For any text field that might have case policies, your test matrix should cover:
| Test | Input | Expected |
|---|---|---|
| Exact case | Original case from registration | Works |
| All uppercase | ADMIN (if registered as admin) |
Pass or fail? |
| All lowercase | admin (if registered as Admin) |
Pass or fail? |
| Mixed case | AdMiN |
Pass or fail? |
| First letter capitalized | Admin vs admin |
Consistent with others? |
Then repeat the matrix across all code paths that accept the same value: registration, login, profile retrieval, search, admin panel, API. The bug is almost always that one path behaves differently from the others.
Unicode Case Folding
Basic case-sensitivity testing covers A-Z / a-z. But Unicode has case mappings for hundreds of other scripts. Some examples that break naive case conversion:
- Turkish
i(U+0069) → uppercaseİ(U+0130, dotted capital I), notI(U+0049). The reverse: TurkishI→ lowercaseı(U+0131, dotless i). AtoUpperCase()that ignores locale converts TurkishinputtoINPUT, but a Turkish locale converts it toİNPUT. - German
ß(U+00DF, sharp s) → uppercase isSS(two characters). A length limit of 10 characters might allowstraße(6 chars) but reject its uppercaseSTRASSE(7 chars). - Greek
Σ(sigma) has two lowercase forms:σ(mid-word) andς(end-of-word). Case-folding must be context-aware to be correct.
For applications with international users, naive toLowerCase() / toUpperCase() is a latent bug. Locale-sensitive case conversion is required for correctness.
Challenge: Restore Password
The practice challenge for this module is the Restore Password flow. As you test it, pay attention not just to whether the form accepts or rejects your input, but to whether the case behavior is consistent across the different steps of the flow.
Ask yourself: if I register with one case and attempt to restore with another, does the system treat them as the same user? Does the behavior match what a user would expect?