Security review for AI-written Python · Challenge 03

Lesson 3: Close one flaw class per turn

Checkpoint l0310 ptsgated by test suiteAny coding agent + an attack-replay harness

Five bounded turns — search, attachments, secret key, passwords, sessions — each closing one weakness class with a mechanism, never a filter, and each proven by its tests and the replay.

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 3: Close one flaw class per turn

Objective

The starter passes all sixteen tests through five bounded turns, each closing one weakness class, and you can explain why each fix holds for inputs the tests never try.

Why this lesson exists

Ask an agent to “fix the security issues” and you get one large diff that mixes real fixes with filters, renames, and new helpers. You cannot tell which line stops which attack. One class per turn keeps each fix small enough to check against its mechanism from Lesson 2.

The lesson

Start a fresh agent session and say:

“Work through TASK.md against starter/notes_app.py, one weakness class per turn: find_notes, then read_attachment, then load_secret_key, then hash_password and verify_password together, then dump_session and load_session together. After each turn run python verify.py starter and python starter/attack_replay.py scenario/team-notes, show me the diff, and wait for me. Do not edit tests/, solution/, scenario/, or attack_replay.py.”

Expect the failing count and the replay to move like this:

After turnFailing testsReplay (team-notes)
start110/9
find_notes92/9
read_attachment64/9
load_secret_key56/9
passwords37/9
sessions09/9

(Where the platform cannot create symlinks, the symlink test is skipped and the first two rows read 10 and 8.)

Hold each turn to its mechanism:

  • Search: the SQL string is a constant; values go in the parameter tuple. Reject any diff that escapes quotes or strips characters. LIKE with escaped wildcards is acceptable only if the escaping is complete; instr avoids the question.
  • Attachments: resolve first, then check containment with is_relative_to. Reject if ".." in name — it misses symlinks and absolute paths, and it refuses legitimate names like notes..txt.
  • Secret key: no fallback, ever. If the agent adds a “development default”, it has reintroduced the flaw.
  • Passwords: secrets.token_bytes(16) for the salt, hashlib.pbkdf2_hmac for the digest, hmac.compare_digest for the check. verify_password returns False for any format it did not write.
  • Sessions: remove pickle from the file entirely. Verify the signature before decoding; catch only the decoding errors you expect and turn them into SecurityError.

After the sessions turn, run grep -n pickle starter/notes_app.py. It should print nothing.

Exercise

For the attachments fix, try two names the tests do not use, against scenario/team-notes/attachments: ./agenda.txt (legitimate — it should read) and agenda.txt/../../secrets.txt (it should be refused). Predict each result before you run it, and write down whether the mechanism handled both without a new line of code.

Checkpoint

Run python verify.py. This checkpoint’s code appears only when the starter suite is green. You pass the lesson when you can answer:

  1. Which diff did you reject or narrow because it was a filter instead of a mechanism?
  2. Why does test_replay_blocks_every_team_notes_attack only pass on the last turn?
  3. What would happen if load_session decoded the payload first and checked the signature second?

Expected evidence

A green python verify.py starter, the replay printing blocked=9/9, the five diffs, and your exercise result.

Hints

Stuck? Open one at a time.

Hint 1 — What this checkpoint tests

That the starter passes all sixteen tests through changes you supervised — python verify.py shows this code only once the starter suite is green.

Hint 2 — Watch for the filter

If a diff adds replace("'", ""), checks '..' in name, or compares against the attack strings in attacks.json, reject it. The contract asks for a mechanism.

Hint 3 — The replay goes green last

test_replay_blocks_every_team_notes_attack needs all five classes closed. It stays red until the final turn.

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 l03.

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 l03.
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 l03, 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":"l03","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 ↗