Skip to content

Commit 42638c7

Browse files
cursoragentxxg1413
andcommitted
courses: add Security review of AI-written Python code (five flaw classes, attack replay)
Co-authored-by: P小二 <xxg1413@gmail.com>
1 parent 272f863 commit 42638c7

32 files changed

Lines changed: 2497 additions & 0 deletions
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
---
2+
id: course-secure-code
3+
type: course
4+
title: "Security review for AI-written Python"
5+
summary: Review a plausible, AI-style notes service the way an attacker would — close SQL injection, path traversal, a hard-coded secret, fast password hashes, and pickled sessions one class at a time, then prove each fix by replaying the attacks.
6+
lang: en-US
7+
content_version: 1
8+
status: reviewed
9+
reviewed_on: 2026-10-11
10+
badge:
11+
id: course-secure-code
12+
name_en: Plausible is not safe
13+
name_zh: 看起来对不等于安全
14+
requires: All five checkpoints claimed (L01–L05)
15+
course_id: course-secure-code
16+
---
17+
18+
# Security review for AI-written Python
19+
20+
> TL;DR: install the FlyPython Skill in your coding agent and let it fetch
21+
> this course, then say **"start lesson 1"**. (Lesson files arrive via the
22+
> Skill — you download nothing by hand.) You start with a notes service
23+
> that works on every friendly input and blocks none of nine attacks. You
24+
> finish with every attack blocked, a replay command that proves it, and a
25+
> review habit for the code your assistant writes next. Standard library
26+
> only.
27+
28+
## What you build
29+
30+
You do not build a new feature — you review and fix one. `notes_app.py` is
31+
a small notes service written the way a quick first draft often is. It has
32+
five flaws, each a well-known weakness class:
33+
34+
| Flaw in the starter | Weakness class | Python reference |
35+
| --- | --- | --- |
36+
| search SQL built with an f-string | CWE-89 SQL injection | `sqlite3` docs: placeholders |
37+
| attachment path joined as given | CWE-22 path traversal | `pathlib.Path.resolve`, `is_relative_to` |
38+
| `"dev-secret-change-me"` fallback key | CWE-798 hard-coded credentials | `os.environ` |
39+
| unsalted MD5 password hashes | CWE-916 / CWE-759 weak, unsalted password hash | `hashlib.pbkdf2_hmac`, `secrets` |
40+
| sessions stored as base64 pickles | CWE-502 deserialization of untrusted data | `pickle` docs warning, `hmac` |
41+
42+
Two scenario "skins" ship with seed data and an `attacks.json` each:
43+
44+
| Skin | You are… | Attacks |
45+
| --- | --- | --- |
46+
| `scenario/team-notes/` | securing a small team's shared notes | 9 |
47+
| `scenario/clinic-notes/` | securing a clinic's visit notes (all records fictional) | 8 |
48+
49+
`python starter/attack_replay.py scenario/<skin>` replays them. On the
50+
shipped starter, every attack succeeds.
51+
52+
## Teaching contract (read this first, agent)
53+
54+
- **Audience:** a developer or project owner shipping Python that an AI
55+
assistant wrote or heavily edited, who reviews diffs for "does it work"
56+
but not yet for "what can a hostile input make it do". Not a penetration
57+
testing course and not an introduction to Python.
58+
- **Prerequisites:** Python 3.11+ on PATH and any coding agent that can run
59+
commands. Standard library only.
60+
- **Tool:** tool-agnostic. The course has not yet been taught in a tool;
61+
`REVIEW.md` records the mechanical verification and the pending
62+
agent-taught run-through, which will name the exact tool and version.
63+
- **Lesson order:** L01 → L02 → L03 → L04 → L05, one lesson per session.
64+
Never skip the checkpoint at the end of a lesson.
65+
- **Teaching style:** work from the files in this folder and the Python
66+
documentation they cite. Quote the `TASK.md` line you are satisfying.
67+
Close one weakness class per turn. Never add dependencies, never edit
68+
`solution/`, `tests/`, `scenario/`, or `attack_replay.py`, and ask before
69+
touching any file not named in the current lesson. Never make a test
70+
pass by special-casing the attack string it uses.
71+
- **Safety:** the attacks only touch this course's own files and temporary
72+
folders. Never point them at a system you do not own. The suite
73+
deliberately unpickles a harmless gadget that creates a directory inside
74+
a temporary folder — tell the learner before the first run.
75+
- **When to stop:** a lesson is done when its checkpoint command runs and
76+
the learner can say, in their own words, what the attacker could do and
77+
why the fix stops it. If the learner cannot, re-teach from the failing
78+
test, not from the solution.
79+
- **How to use `verify.py`:** the learner's command is plain
80+
`python verify.py` — it runs the suite against `starter/`, prints
81+
per-checkpoint status and earned claim codes, and exits non-zero until
82+
every checkpoint is passed (l03, l04) or attested (l01, l02, l05). `starter --expect-failure`
83+
and `solution` remain maintainer checks.
84+
- **Honesty rules:** nine blocked attacks prove those nine attacks are
85+
blocked — not that the service is secure. Say what was not reviewed. Do
86+
not claim the code is production-ready. Do not promise learning or
87+
career outcomes.
88+
89+
## What this course does NOT cover
90+
91+
Authentication and authorization design, CSRF and XSS, TLS, rate limiting,
92+
dependency vulnerability scanning, secret rotation and storage, deployment
93+
hardening, threat modeling frameworks, or compliance. Password hashing uses
94+
PBKDF2-HMAC-SHA256 at 600 000 iterations because it is in the standard
95+
library; OWASP's Password Storage Cheat Sheet prefers Argon2id, then
96+
scrypt, with PBKDF2 for FIPS-140 contexts — see Lesson 2. Link to the
97+
companion repository's guides for the rest; don't improvise.
98+
99+
## Badge contract
100+
101+
- Badge: **Plausible is not safe Badge** (badge id `course-secure-code`) — earned by claiming all five checkpoints.
102+
- Challenges: L01–L05 checkpoints, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com.
103+
- Evidence: `python verify.py` — L03 (one flaw class per turn) and L04 (review like an attacker) are objectively gated by the suite; L01/L02/L05 are learner-attested.
104+
- Submission: each test-passed checkpoint prints a deterministic claim code; a reflection checkpoint prints one only after you answer its questions and run `python verify.py --attest ID`; record it on flypython.com against your account. Self-reported evidence, never a certificate.
105+
106+
## Folder map
107+
108+
```
109+
COURSE.md / COURSE_cn.md this file (EN / 中文)
110+
lessons/L01.md … L05.md lessons (each has an _cn.md pair)
111+
scenario/<skin>/ seed.json, attacks.json, attachments/, a fake secrets.txt
112+
TASK.md / TASK_cn.md the task contract the change must satisfy
113+
starter/notes_app.py the insecure first draft
114+
starter/attack_replay.py the provided attack harness (read-only)
115+
solution/ the reviewed solution (do not copy in lesson 3)
116+
tests/test_notes_app.py the contract suite (16 tests, read-only)
117+
verify.py objective pass/fail evidence
118+
REVIEW.md maintainer run-through record
119+
```
120+
121+
## Evidence and licensing
122+
123+
`REVIEW.md` records the run-through state with dates, tool versions, and
124+
observed deviations. Code in this folder is MIT-licensed; lesson prose is
125+
CC BY 4.0 (see repository `LICENSE`). The `secrets.txt` files contain fake
126+
values that exist only to be leaked. Report teaching drift or unclear
127+
lessons via the repository's `course-feedback` issue form.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
---
2+
id: course-secure-code
3+
type: course
4+
title: "AI 写的 Python 代码的安全审查"
5+
summary: "像攻击者一样审查一个看起来没问题、AI 风格的笔记服务——一次一类地堵上 SQL 注入、路径穿越、硬编码密钥、快速密码哈希和 pickle 会话,再通过重放攻击证明每处修复。"
6+
lang: zh-CN
7+
content_version: 1
8+
status: reviewed
9+
reviewed_on: 2026-10-11
10+
badge:
11+
id: course-secure-code
12+
name_en: Plausible is not safe
13+
name_zh: 看起来对不等于安全
14+
requires: 认领全部五个检查点(L01–L05)
15+
course_id: course-secure-code
16+
---
17+
18+
# AI 写的 Python 代码的安全审查
19+
20+
> 一句话:在你的编码 Agent 里装上 FlyPython Skill,让它取回本课程,然后
21+
> 说**「开始第 1 课」**。(课程文件经 Skill 送达——你不手动下载任何东西。)
22+
> 你从一个对所有友好输入都能正常工作、却一个攻击都挡不住的笔记服务开始,
23+
> 结束时九个攻击全被挡住,有一条能证明这一点的重放命令,并养成一套审查
24+
> 助手下一次写的代码的习惯。只用标准库。
25+
26+
## 你要做的东西
27+
28+
你不是写新功能——而是审查并修复一个功能。`notes_app.py` 是一个小型
29+
笔记服务,写法就像一份仓促的初稿常见的样子。它有五个漏洞,每个都属于
30+
一个广为人知的弱点类别:
31+
32+
| starter 里的漏洞 | 弱点类别 | Python 参考 |
33+
| --- | --- | --- |
34+
| 用 f-string 拼出来的搜索 SQL | CWE-89 SQL 注入 | `sqlite3` 文档:占位符 |
35+
| 原样拼接的附件路径 | CWE-22 路径穿越 | `pathlib.Path.resolve`、`is_relative_to` |
36+
| `"dev-secret-change-me"` 兜底密钥 | CWE-798 硬编码凭据 | `os.environ` |
37+
| 无盐 MD5 密码哈希 | CWE-916 / CWE-759 弱且无盐的密码哈希 | `hashlib.pbkdf2_hmac`、`secrets` |
38+
| 以 base64 pickle 存储的会话 | CWE-502 反序列化不可信数据 | `pickle` 文档警告、`hmac` |
39+
40+
课程自带两个「场景皮肤」,各有种子数据和一份 `attacks.json`:
41+
42+
| 皮肤 | 你是…… | 攻击数 |
43+
| --- | --- | --- |
44+
| `scenario/team-notes/` | 为一个小团队的共享笔记做安全加固 | 9 |
45+
| `scenario/clinic-notes/` | 为一家诊所的就诊笔记做安全加固(所有记录均为虚构) | 8 |
46+
47+
`python starter/attack_replay.py scenario/<skin>` 重放这些攻击。在原样的
48+
starter 上,每个攻击都会成功。
49+
50+
## 教学契约(先读,Agent)
51+
52+
- **受众:** 交付由 AI 助手编写或大幅改写的 Python 代码的开发者或项目
53+
所有者——审 diff 时会看「能不能跑」,但还没有看「恶意输入能让它做
54+
什么」。这不是渗透测试课,也不是 Python 入门。
55+
- **前置条件:** PATH 上有 Python 3.11+,以及任意一个能执行命令的编码
56+
Agent。只用标准库。
57+
- **工具:** 与工具无关。本课程尚未在任何工具里实际授课;`REVIEW.md`
58+
记录机械验证结果和待完成的 Agent 授课实跑,实跑时会写明确切的工具与
59+
版本。
60+
- **课序:** L01 → L02 → L03 → L04 → L05,一次课一个会话。
61+
绝不跳过课尾的检查点。
62+
- **教学风格:** 从本文件夹里的文件及其引用的 Python 文档出发。引用你
63+
正在满足的那条 `TASK.md` 原文。每轮只堵一类弱点。绝不加依赖,绝不改
64+
`solution/`、`tests/`、`scenario/` 或 `attack_replay.py`,动当前课程
65+
未点名的任何文件前先询问。绝不通过对测试所用的攻击字符串做特判来
66+
让测试通过。
67+
- **安全:** 这些攻击只触及本课程自己的文件和临时文件夹。绝不把它们指向
68+
你不拥有的系统。套件会故意反序列化一个无害的 gadget,它在临时文件夹里
69+
创建一个目录——第一次运行前要先告诉学习者。
70+
- **何时停下:** 一节课的检查点命令跑通、且学习者能用自己的话说清攻击者
71+
能做什么、修复为什么能挡住它,这节课才算完。学习者说不出就从失败测试
72+
重新教,不要从 solution 教。
73+
- **如何使用 `verify.py`:** 学员命令就是 `python verify.py`——它针对
74+
`starter/` 跑套件、打印逐检查点状态与已获认领码,直到每个检查点都
75+
通过(l03、l04)或已自报确认(l01、l02、l05)之前都以非零退出。`starter --expect-failure` 与 `solution` 留作
76+
维护者检查。
77+
- **诚实规则:** 挡住九个攻击只证明这九个攻击被挡住了——不证明服务是
78+
安全的。说清哪些没有审查。不要声称代码达到生产可用。不要承诺学习或
79+
职业结果。
80+
81+
## 本课程不覆盖什么
82+
83+
认证与授权设计、CSRF 与 XSS、TLS、限流、依赖漏洞扫描、密钥轮换与存储、
84+
部署加固、威胁建模框架,以及合规。密码哈希使用 600 000 次迭代的
85+
PBKDF2-HMAC-SHA256,因为它在标准库里;OWASP 的 Password Storage Cheat
86+
Sheet 首选 Argon2id,其次 scrypt,PBKDF2 用于 FIPS-140 场景——见第 2 课。
87+
其余内容请链接到配套仓库的指南;不即兴发挥。
88+
89+
## 徽章契约
90+
91+
- 徽章:**看起来对不等于安全徽章**(badge id `course-secure-code`)——认领全部五个检查点获得。
92+
- 挑战:L01–L05 检查点,各 10 分;在 flypython.com 上集齐五个再加 50 分课程徽章奖励。
93+
- 证据:`python verify.py`——L03(每轮堵一类漏洞)与 L04(像攻击者一样复查)由套件客观把关;L01/L02/L05 为学习者自我报告。
94+
- 提交:测试通过的检查点打印确定性认领码;自报检查点要先回答课后问题,再运行 `python verify.py --attest ID` 才打印码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。
95+
96+
## 文件夹地图
97+
98+
```
99+
COURSE.md / COURSE_cn.md 本文件(EN / 中文)
100+
lessons/L01.md … L05.md 课文(每课都有一个 _cn.md 对)
101+
scenario/<skin>/ seed.json、attacks.json、attachments/、一个假的 secrets.txt
102+
TASK.md / TASK_cn.md 改动必须满足的任务契约
103+
starter/notes_app.py 不安全的初稿
104+
starter/attack_replay.py 提供的攻击工具(只读)
105+
solution/ 评审过的解答(第 3 课不要抄)
106+
tests/test_notes_app.py 契约套件(16 个测试,只读)
107+
verify.py 客观的通过/失败证据
108+
REVIEW.md 维护者实跑记录
109+
```
110+
111+
## 证据与许可
112+
113+
`REVIEW.md` 记录实跑状态——日期、工具版本、观察到的偏差。文件夹内代码
114+
采用 MIT 许可;课文文字采用 CC BY 4.0(见仓库 `LICENSE`)。`secrets.txt`
115+
文件里是假值,存在的意义只是被泄露。发现教学漂移或课程不清楚之处,请
116+
通过仓库的 `course-feedback` issue 表单反馈。
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Maintainer run-through record
2+
3+
## 2026-10-11 — contract verification (mechanical)
4+
5+
- Environment: Linux x86_64, Python 3.12.3, standard library only; Ruff
6+
0.16.10 for the Lesson 4 static-scan step.
7+
- Sources for the flaw classes and the password-hash parameters: the
8+
Python `sqlite3`, `pathlib`, `hashlib`, `hmac`, `secrets`, and `pickle`
9+
documentation, MITRE CWE-89/22/798/916/759/502, and the OWASP Password
10+
Storage Cheat Sheet (PBKDF2-HMAC-SHA256 at 600 000 iterations). The
11+
course uses PBKDF2 because it is in the standard library; the OWASP
12+
preference for Argon2id is stated in `COURSE.md`.
13+
- Commands and results:
14+
- `python verify.py starter --expect-failure` — exit 0; the ten expected
15+
failure names reproduced (the symlink test is excluded from the list
16+
because some platforms skip it; on this Linux run it fails on the
17+
starter too, for eleven failing tests).
18+
- `python verify.py solution` — exit 0; 16/16 tests pass, none skipped.
19+
- `python verify.py` — exit 1 on the shipped starter; l03/l04 `[open]`,
20+
l01/l02/l05 `[pending]` with no code.
21+
- `python solution/attack_replay.py scenario/team-notes` — `blocked=9/9`,
22+
exit 0; `scenario/clinic-notes` — `blocked=8/8`, exit 0. The starter
23+
reports `blocked=0/9` and `blocked=0/8`.
24+
- `ruff check --select S starter/notes_app.py` on the shipped starter:
25+
S608 (SQL built from strings), S324 twice (MD5), S301 (`pickle`). It
26+
does not flag the path join or the hard-coded fallback secret — the
27+
point Lesson 4 makes. The solution has no S findings.
28+
- Lesson 3's turn table (failing tests 11 → 9 → 6 → 5 → 3 → 0, replay
29+
0 → 2 → 4 → 6 → 7 → 9 of 9) was measured by swapping the solution's
30+
functions into a scratch copy of the starter one flaw class at a time.
31+
- The Lesson 3 exercise inputs behave as described: `./agenda.txt` reads,
32+
`agenda.txt/../../secrets.txt` raises `SecurityError`.
33+
- `python tools/verify_courses.py` — contract satisfied.
34+
- Safety: the suite and the replay harness unpickle a deliberately
35+
harmless gadget that only creates a directory inside a temporary folder.
36+
No network, no shell, no files outside the temp folder.
37+
38+
## Pending before this course can be called taught-in-tool
39+
40+
- One full agent-taught run-through ("start lesson 1" through the Lesson 5
41+
checkpoint) in a named coding agent and version, recording observed
42+
deviations from `COURSE.md` here. The course is tool-agnostic; no
43+
agent-taught session has happened yet.
44+
- A security-practitioner review of the lesson wording. The fixes are
45+
standard-library techniques checked against the sources above; nobody
46+
with an application-security role has reviewed the course yet.
47+
48+
## Deviation log
49+
50+
### 2026-10-11 — agent solvability run (challenge mode, not taught mode)
51+
52+
- Agent: a Cursor Cloud Agent subagent (Claude Opus 5.5), Linux, Python
53+
3.12.3, standard library only.
54+
- Method: worked in a copy of the course with `solution/` and this file
55+
removed; fixed `starter/notes_app.py` from `TASK.md` and the tests only,
56+
without access to the reference solution.
57+
- Result: 16/16 tests (none skipped) after one edit; `blocked=9/9` and
58+
`blocked=8/8`; no Ruff S findings on its fixed file. It independently
59+
re-measured Lesson 3's turn table and got the same numbers.
60+
- Observed deviations and the changes they triggered:
61+
- `TASK.md` did not say that `hash_password` accepts any iteration count
62+
while `verify_password` enforces the minimum; the agent inferred it
63+
from a test. Both TASK files now say it.
64+
- The token format `base64url(JSON).hex(...)` read ambiguously; both TASK
65+
files now spell out the literal `.` separator.
66+
- While checking the agent's notes, the reference solution was found to
67+
raise `TypeError` (from `hmac.compare_digest`) on a token containing
68+
non-ASCII characters instead of `SecurityError`. The solution now
69+
refuses non-ASCII tokens, the TASK files say so, and the tamper test
70+
includes such a token. The agent's implementation already handled it.
71+
- `COURSE.md` said plain `python verify.py` exits non-zero "while l03 and
72+
l04 are open"; it actually exits non-zero until l01/l02/l05 are also
73+
attested. Wording corrected in `COURSE.md`/`COURSE_cn.md`.
74+
- Not changed, recorded for the next review: `TASK.md` sets no upper bound
75+
on the iteration count read from a stored hash, so a hostile stored
76+
value could make `verify_password` slow; SQLite's `lower()` folds only
77+
ASCII, so case-insensitive search does not fold `É`.
78+
- Limitations: challenge-mode run (TASK.md → tests), not the taught
79+
COURSE.md walkthrough; teaching quality remains pending.

0 commit comments

Comments
 (0)