Lab β Red-Team Your Own LLM API#
Attack a system you built, prove which defence stopped each attack, and leave behind a regression suite that keeps it fixed.
β± ~4β5 hours π needs: LLM Security β Offensive Β· LLM Safety β Defensive Β· OWASP LLM Top 10
You’ll build a deliberately weak LLM service, break it, harden it, and then prove the hardening works β with tests that run in CI.
βοΈ Target only your own deployment. Every attack in this lab runs against the service you build and host. Probing a third party’s LLM product is unauthorised testing. If you want a harder target, make your own service harder.
Part 1 β Build the vulnerable service (45 min)#
A FastAPI app with an LLM endpoint that deliberately commits several OWASP sins:
- A system prompt containing a fake secret (
the internal API key is DEMO-1234) - A tool the model can call (
send_email,delete_recordβ stubbed, printing rather than acting) - A
/summarise?url=endpoint that fetches a page and feeds the text to the model β your indirect-injection surface - No output validation, no budget cap
Use fake secrets and stubbed tools throughout. Nothing here should be able to do anything real.
Part 2 β Attack it (90 min)#
Write at least 12 attacks across these classes, and record the outcome of each:
| Class | OWASP | Attempts |
|---|---|---|
| System prompt extraction | LLM07 | β₯3 (direct, role-play, “summarise your instructions”) |
| Direct prompt injection | LLM01 | β₯2 |
| Indirect injection | LLM01 | β₯3 β host an HTML file with hidden instructions and point /summarise at it |
| Tool abuse | LLM06 | β₯2 β make the model call delete_record unprompted by the user |
| Output-handling abuse | LLM05 | β₯1 β get markdown-image exfiltration or script into the output |
| Unbounded consumption | LLM10 | β₯1 β make one request cost far more than it should |
Record each as: id | class | payload | result | evidence. Indirect injection is the centrepiece β the payload lives in a page the model reads, not in anything the user typed.
Part 3 β Defend (90 min)#
Apply the layers from LLM Safety β Defensive, in this order:
- Remove secrets from the system prompt entirely
- Tool allow-list, deny by default, with a human gate on destructive tools
- Schema validation on every tool call
- Delimit and label untrusted retrieved content
- Escape/sanitise output before rendering; restrict outbound domains
- Token and spend caps
For each attack from Part 2, re-run it and record which specific layer stopped it. An attack blocked by two layers is worth noting β that’s defence in depth working.
Part 4 β Lock it in (45 min)#
Turn every successful attack into an automated test:
@pytest.mark.parametrize("payload", INJECTION_PAYLOADS)
def test_no_secret_leak(payload):
r = client.post("/ask", json={"question": payload})
assert "DEMO-1234" not in r.text
assert "system prompt" not in r.text.lower()Wire it into GitHub Actions so it runs on every push. Optionally add promptfoo for a broader generated attack set.
Deliverables#
| # | Item |
|---|---|
| 1 | Repo with vulnerable/ and hardened/ (or a feature flag) |
| 2 | ATTACKS.md β the β₯12 attacks, before/after results, evidence |
| 3 | A defence matrix: attack Γ layer that stopped it |
| 4 | A passing test suite, running in CI |
| 5 | 200 words: which attack was hardest to stop, and why |
Grading#
| Weight | Criterion |
|---|---|
| 25% | Attack quality β genuine variety, including working indirect injection |
| 25% | Defence mapping β you can name which layer stopped what, with evidence |
| 20% | Regression suite β tests exist, pass, and run in CI |
| 15% | Layered thinking β capability limits, not just input filters |
| 15% | Write-up β honest about what still gets through |
Being unable to fully stop prompt injection is the expected result. Saying so, and showing that your capability limits make it harmless, is the correct answer. Claiming you solved it is the wrong one.
Checklist#
- Fake secrets and stubbed tools only.
- At least one indirect injection that works via a page the model reads.
- Every successful attack has a test.
- Tests run in CI on push.
- The write-up names what still gets through.