Lesson 2: Write the threat model as a contract
Objective
You can say, for each function, who controls each input and what they
want; trace every refusal in TASK.md to a test; and name the standard
library mechanism that makes the refusal reliable rather than lucky.
Why this lesson exists
“Make it secure” is the vaguest request you can give an agent. It produces
input filters that strip quotes, check for .., or look for the word
admin — each of which an attacker routes around. A threat model written
as a contract says what must be refused and lets the tests check it; the
mechanism you choose then decides whether the refusal holds for inputs
nobody wrote a test for.
The lesson
Read TASK.md with the tests beside it. For each function, the contract
answers three questions:
| Function | Who controls the input | What they want | Mechanism that refuses it |
|---|
find_notes | the person searching | other owners’ notes | ? placeholders — the sqlite3 docs’ recommended way to bind values; plus instr so % and _ stay literal |
read_attachment | whoever names the file | any file the process can read | Path.resolve() then is_relative_to(base) — checks where the path really goes, symlinks included |
load_secret_key | whoever reads your source code | the signing key | no default; refuse to start below 32 characters |
hash_password / verify_password | whoever steals the database | the passwords | salted, slow PBKDF2 via hashlib.pbkdf2_hmac; hmac.compare_digest for the comparison |
dump_session / load_session | whoever holds the cookie | another identity — or code execution | JSON plus an HMAC-SHA256 signature checked before decoding; never pickle |
The Python documentation is the primary source for each choice:
pickle: the module documentation opens with a warning that pickle is
not secure and that you should only unpickle data you trust. A session
token comes from a client — it is never data you trust.
sqlite3: the documentation’s how-to on placeholders shows binding
values with ? and warns against assembling SQL with string formatting.
hmac.compare_digest: compares in a way designed to resist timing
attacks; == on secrets does not.
secrets: the module for security tokens and salts — not random.
On password hashing, the contract chooses PBKDF2-HMAC-SHA256 at 600 000
iterations because it is in the standard library and it is the setting
the OWASP Password Storage Cheat Sheet gives for PBKDF2. The same cheat
sheet prefers Argon2id, then scrypt, and recommends PBKDF2 where FIPS-140
compliance is required. In a real project, decide which applies to you.
Exercise
Pick a function in a project you know that takes input from a request,
a file, or a message queue. Fill in the same three columns for it. Then
check: is your mechanism a mechanism, or a filter that removes the attack
strings you happened to think of?
Checkpoint
When you can answer these, claim the checkpoint with
python verify.py --attest l02:
- What does
Path(base) / "/etc/passwd" evaluate to, and why does the
contract refuse every absolute name — even one that happens to point
inside base_dir?
- Why must
load_session check the signature before it decodes the
payload?
- Why is “strip quotes from the query” not an acceptable fix for
find_notes?
Expected evidence
Your three-column threat model for one function you own, with the
mechanism marked as a mechanism or a filter.