Security review for AI-written Python · Challenge 02

Lesson 2: Write the threat model as a contract

Checkpoint l0210 ptsself-reported — code from verify.pyAny coding agent + an attack-replay harness

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.

Doing this with your agent? One sentence starts the whole course:

Read https://flypython.com/skills/flypython/SKILL.md and start the FlyPython course `secure-ai-generated-code`.

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:

FunctionWho controls the inputWhat they wantMechanism that refuses it
find_notesthe person searchingother owners’ notes? placeholders — the sqlite3 docs’ recommended way to bind values; plus instr so % and _ stay literal
read_attachmentwhoever names the fileany file the process can readPath.resolve() then is_relative_to(base) — checks where the path really goes, symlinks included
load_secret_keywhoever reads your source codethe signing keyno default; refuse to start below 32 characters
hash_password / verify_passwordwhoever steals the databasethe passwordssalted, slow PBKDF2 via hashlib.pbkdf2_hmac; hmac.compare_digest for the comparison
dump_session / load_sessionwhoever holds the cookieanother identity — or code executionJSON 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.

Hints

Stuck? Open one at a time.

Hint 1 — 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.

Hint 2 — 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.

Hint 3 — 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.

Submit

Done? Record it.

Use the 8-character code printed by python verify.py. For a self-reported checkpoint, answer the lesson questions first, then run python verify.py --attest l02.

Youbrowser

Submit the claim code

Your agentauto-submit

Let the agent solve and submit

Hand it this checkpoint (copy button on hover) — it solves, verifies, and submits on its own:

Work on the FlyPython challenge "Security review for AI-written Python" (course id course-secure-code), checkpoint l02.
Machine-readable brief: https://flypython.com/api/challenges/secure-ai-generated-code
Open the course folder and read TASK.md first — it is the contract.
Rules: smallest change, no new dependencies, never edit tests/ or solution/.
Check starter/ with python verify.py; ask me the lesson questions before using --attest ID for any self-reported checkpoint.
Target: solve only checkpoint l02, and submit it as soon as it passes.

With a token from your agent page (env FLYPYTHON_TOKEN), it records the result directly:

curl -X POST https://flypython.com/api/claims \
  -H "Authorization: Bearer $FLYPYTHON_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"claims":[{"course":"secure-ai-generated-code","checkpoint":"l02","code":"<8-char code>"}]}'

Or install the full FlyPython agent skill once and skip the paste.

Files

The starter and verifier

Solving runs on your machine — your agent fetches the course files itself via /api/challenges/secure-ai-generated-code/files; you download nothing by hand. Prefer reading the source? Browse the folder on GitHub ↗