Skip to content

Latest commit

 

History

History
89 lines (71 loc) · 4.45 KB

L02.md

File metadata and controls

89 lines (71 loc) · 4.45 KB
id course-secure-code-l02
type course
title Lesson 2: Write the threat model as a contract
summary For each function, name who controls the input, what they want, and what the code must refuse — then trace every refusal in TASK.md to a test and to the Python documentation behind it.
lang en-US
content_version 1
status reviewed
reviewed_on 2026-10-11
hints
title body
What this checkpoint tests
That you can read `TASK.md` as a list of refusals, trace each to a test, and say which Python feature makes the refusal reliable.
title body
Trust boundaries first
In `read_attachment(base_dir, name)`, `base_dir` comes from your configuration and `name` comes from a request. The contract treats them differently on purpose.
title body
Prefer the mechanism to the filter
Bound parameters, resolved paths, keyed signatures: each removes a whole class of attack. A blocklist of bad strings removes the strings you thought of.

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:

  1. 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?
  2. Why must load_session check the signature before it decodes the payload?
  3. 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.