Blocklists Enumerate Badness and Always Miss Something

Key takeaway: A blocklist must be complete to work and can never be proven complete. An allowlist is complete by construction, because it describes what you accept rather than what you reject.
Why Rejection Lists Fail
Consider filtering a script tag from user input. Stripping the literal string leaves case variations, whitespace inside the tag, null bytes, HTML entity encoding, double URL encoding, and forms that do not use that tag at all — an image element with an error handler, an inline event attribute, a javascript: URL.
The pattern generalises to every injection class. SQL keyword filtering misses comment-interrupted variants and alternative syntax. Path traversal filtering misses encoded separators, overlong UTF-8 and Windows short names. Command filtering misses shell substitution and quoting.
The structural problem is asymmetry of effort. You must anticipate every representation; the attacker needs one you did not. That contest has no stable winning position.
What Acceptance Looks Like
An allowlist inverts the burden. A postcode field accepts a specific character set and length. A country field accepts one of a fixed list. A sort parameter accepts one of the column names you actually support. A file upload accepts three extensions with verified content types.
Anything not matching is rejected without needing to know why it was sent. A novel encoding fails automatically because it is not on the list, so the defence does not decay as attack techniques evolve.
| Field | Blocklist approach | Allowlist approach |
|---|---|---|
| Username | Strip dangerous characters | ^[a-z0-9_]{3,20}$ |
| Sort column | Remove SQL keywords | One of an explicit set |
| Redirect target | Block external hosts | One of known internal paths |
| File type | Reject .exe, .sh |
Accept .pdf, .png, .jpg |
| Numeric ID | Escape quotes | Parse as integer, range check |
The numeric case shows the principle most clearly. Parsing to an integer and range-checking eliminates injection entirely, because the value is no longer a string by the time it reaches the query. Type conversion is the strongest form of validation available.
Validation Is Not Escaping
Confusing the two causes real vulnerabilities, because they solve different problems at different points.
Validation happens at the boundary and decides whether input is acceptable. Escaping happens at the point of use and makes data safe for a specific destination. The same value needs different escaping for HTML, for a SQL query, for a shell command and for a JSON document.
Validating once and treating the value as universally safe fails as soon as it reaches a context you did not consider. The correct discipline is validating on entry and encoding contextually on every output — parameterised queries for SQL, context-aware templating for HTML, argument arrays rather than shell strings for subprocess calls.
Making It Sustainable
Define validation with a schema at the boundary rather than scattering checks through handlers. A declared schema is reviewable, testable, and applies uniformly instead of depending on each developer remembering.
Reject rather than sanitise. Silently removing characters produces surprising results and can construct a payload from an apparently safe input. Returning a clear error is better for security and for the user.
Validate length explicitly. Unbounded input causes memory exhaustion and enables payloads that pattern checks were not sized for.
Apply the same rules server-side regardless of client validation. Client checks improve usability and provide no security, since the client is under the attacker’s control.
The Bottom Line
Specify what each field accepts and reject everything else, converting to a concrete type wherever possible. Then escape at every output boundary according to that destination, because validation and encoding are separate obligations and neither substitutes for the other.



