A vulnerability you fixed without a test is a vulnerability that will come back. Refactors, merges, and the next assistant-written PR all erode fixes that nothing guards.
A security regression test is the guard: a test that fails against the vulnerable code and passes against the patched code, wired into CI so any future reintroduction breaks the build. AI is excellent at drafting these — it can turn an exploit path into a concrete test case quickly — but the test is only trustworthy once you've proven it actually fails on the old code. A green test that would stay green even if the bug returned is worse than no test, because it manufactures false confidence.
The defining check for a security regression test is the red-then-green proof: it MUST fail against the vulnerable version and pass against the fix. A test that passes on both proves nothing and lies to everyone who reads the green checkmark.
Start from the confirmed exploit, not the fix. You need the exact input that triggered the vulnerability — the payload, the request, the state — because that is what the test must exercise.
Prompt for a test that encodes the exploit as an assertion:
You are writing a security regression test for a vulnerability I fixed in
my own code. Write a test that:
- exercises the exact exploit path (the malicious input that triggered it)
- asserts the SECURE behavior (input rejected / query parameterized /
access denied), so it FAILS on the vulnerable version and PASSES on the
fixed version
- is deterministic and needs no external services
Name the CWE in a comment. Use <pytest / the repo's framework>.
VULNERABILITY: path traversal (CWE-22) in /download?file=
EXPLOIT INPUT: file=../../etc/passwd
FIXED BEHAVIOR: request returns 403, no file outside the allowed dir
FIXED CODE:
<paste the patched handler>
Expected output: a self-contained test that sends the traversal payload and asserts a 403 (or that no out-of-tree path is read) — an assertion that is false on the old code and true on the new.
Prove it fails on the vulnerable code — the non-negotiable step. Check out the pre-fix commit (or revert the patch locally) and run the new test. It must fail. If it passes against the vulnerable code, the test is not testing the vulnerability — send it back:
git stash # set aside the fix
pytest tests/test_download_traversal.py # MUST fail here
git stash pop # restore the fix
pytest tests/test_download_traversal.py # MUST pass here
Parameterize for the family where the bug recurs. If the same class appears in several places, ask the model to expand the payload set (encoded traversal %2e%2e%2f, absolute paths, null bytes) so one test covers the variants an attacker would actually try.
Wire it into CI as a gate. Add the test to the security suite and make its failure block merge — that is what turns a one-time fix into a durable control.
Regression tests embed the client's code and a working exploit payload against their system. Before prompting, redact real paths, hosts, and credentials, and reduce the handler to the security-relevant logic. If external tools are barred, run a local model for the test drafting — the test runner and Semgrep execute entirely offline. Never paste a live production exploit into a public tool; reproduce the pattern against a local sandbox instead. Keep tests in the client's own repository and CI, and confirm per-client tool approval before prompting.