Chapter 9 — Programming II
Lesson map
Lesson 09 published. Map, example, EX025.
flowchart TD
N0["Learning outcomes"]
N1["9.1 Allowlists"]
N2["Lab — Lab — validating fields"]
N3["9.2 Lab result"]
N4["9.3 Where to validate"]
N5["9.4 Error handling"]
N6["9.5 Testing rules"]
N7["Canonical forms"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6 --> N7
N7 --> EX["Worked example"]
EX --> Q["Exercise EX025"]
Logical drawing
flowchart LR
N0["Learning outcomes"]
N1["9.1 Allowlists"]
N2["Lab — Lab — validating fields"]
N3["9.2 Lab result"]
N4["9.3 Where to validate"]
N5["9.4 Error handling"]
N6["9.5 Testing rules"]
N7["Canonical forms"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6 --> N7 --> Q["EX025"]
[Learning outcomes]
|
v
[9.1 Allowlists]
|
v
[Lab — Lab — validating fields]
|
v
[9.2 Lab result]
|
v
[9.3 Where to validate]
|
v
[9.4 Error handling]
|
v
[9.5 Testing rules]
|
v
[Canonical forms]
|
v
[Worked example] --> [Exercise EX025]
Learning outcomes
You will (1) validate input with allowlists; (2) separate parsing from business logic; (3) handle invalid input safely; (4) write tests for validation rules.
9.1 Allowlists
An allowlist says exactly what is acceptable: zones A to D, one to nine passengers, four digits for card endings. Anything else is rejected. This is safer than trying to list everything bad. ASVS 4.0.3's validation chapter (V5) and the OWASP Top 10:2021 category A03 Injection both stress validating and handling input safely.
A denylist for zones might reject quotes and angle brackets, but it would still accept 'E' or 'AA', and attackers keep finding characters the list forgot. The allowlist states the four valid letters and nothing else gets through. The rules in the lab are anchored at both ends, so extra characters before or after a valid value also fail.
Lab — Lab — validating fields
"""Allowlist validation of synthetic ticket form fields."""
import re
rules = {"zone": re.compile(r"^[A-D]$"), "passengers": re.compile(r"^[1-9]$"), "card_last4": re.compile(r"^\d{4}$")}
inputs = [{"zone": "B", "passengers": "2", "card_last4": "4242"}, {"zone": "Z", "passengers": "2", "card_last4": "4242"},
{"zone": "A", "passengers": "12", "card_last4": "42a2"}]
for i, form in enumerate(inputs, 1):
bad = [k for k, rx in rules.items() if not rx.fullmatch(form.get(k, ""))]
print(f"form {i}: {'accepted' if not bad else 'rejected ' + ', '.join(bad)}")
Run with python3 lab_validate.py. Verified output:
form 1: accepted
form 2: rejected zone
form 3: rejected passengers, card_last4
9.2 Lab result
Form 1 is accepted. Form 2 is rejected because zone Z is not allowed. Form 3 is rejected for two fields: 12 passengers exceeds the single-digit rule and "42a2" is not four digits.
Notice that form 3 reports both failing fields rather than stopping at the first. Users fix all problems in one attempt, and the log shows every rule that fired. Form 2 shows that a value can be well formed, a single capital letter, and still be wrong for the business: Z is a letter but not a zone. The output also confirms fullmatch is used, so partial matches are not accepted.
9.3 Where to validate
Validation happens on the server; browser checks improve user experience but can be bypassed. Validation is also not the only defence: output must still be escaped and queries parameterised (chapter 13).
Anyone can send requests straight to the server with a script, skipping the browser entirely, so the server-side rules are the ones that count. Keeping the rule list in one shared module stops the browser and server drifting apart. Validation narrows what reaches the rest of the code; escaping and parameterised queries still handle values that are valid but contain characters such as apostrophes in surnames.
9.4 Error handling
Rejected forms return a clear message naming the field, without echoing the raw input. Logs record the rejection without storing card data.
Echoing raw input back can turn an error page into a place where injected markup runs, so the message says 'zone must be A to D' instead of repeating what was sent. Messages also avoid revealing internal details such as stack traces or table names. The log line records the field and the rule that failed, which is enough to spot a pattern of probing without collecting card endings.
9.5 Testing rules
Each rule has tests for boundaries: zone A and D accepted, E rejected; 1 and 9 passengers accepted, 0 and 10 rejected.
Boundary tests catch the commonest mistakes, such as writing a range that excludes its last value or a pattern without anchors. Kwame adds empty strings, missing fields and very long values, since forms sent by scripts may contain any of them. When a business rule changes, the test changes in the same commit, so reviewers see the old and new limits side by side.
Canonical forms
Before validating, input is converted to a standard form: trimmed of spaces and in one character encoding. Otherwise two strings that look the same could be treated differently, letting some values slip past checks. Kwame adds a test where the zone value has a trailing space; after normalising it is accepted as B.
Worked example
A customer complains that groups of 12 cannot buy one ticket. Product decides groups over 9 must use the group booking page. The rule stays, the error message now links to group booking, and a test confirms the link appears.
Synthetic case. Forms and complaints are synthetic.
Errors and limits
Allowlists must be kept in sync with business rules or legitimate users are blocked.
Exercises
- EX025 — Allowlist meaning. What is an allowlist? Rubric: 3 = exactly what is acceptable; reject rest; 2 = partial; 1 = vague; 0 = none.
- EX026 — Lab rejections. Why was form 3 rejected? Rubric: 3 = passengers 12; card_last4 not digits; 2 = partial; 1 = vague; 0 = none.
- EX027 — Server side. Why validate on the server? Rubric: 3 = browser checks bypassable; 2 = partial; 1 = vague; 0 = none.
Synthesis
Validate with allowlists on the server, test boundaries and keep rules aligned with the business.
Continue studying — chapter 09 of 50
← Chapter 08 (draft) · Course chapter index · Chapter 10 (draft) →