Skip to main content

All weeks · Worksheet · Overview · Attack notes

Week 14 · Lecture slides

Week 14

Contents20 sections

Security of AI / LLM-Powered Applications

Software Security · Nutthakorn Chalaemwongwan


Today

  • Where AI features add attack surface
  • OWASP LLM Top 10 (2025)
  • Prompt injection (direct + indirect)
  • AI agents with tools (MCP = Model Context Protocol) — risks
  • 🎮 Game: Gandalf Challenge — now on Lakera Agent Breaker

Why a whole week on AI

  • LLMs (large language models) now sit inside real products & agents
  • New, fast-moving attack surface
  • OWASP LLM Top 10 + MITRE ATLAS

One app, three layers. You are here: W14. W13, where it runs: a secret baked into an image layer and an IAM policy with Action * on Resource *; fix with a hardened image and least privilege. W14, what it says: the app gets a chatbot; typed text overrides its rules (prompt injection) and its reply goes into the page unescaped (XSS); fix with guardrails, secret redaction and output escaping. W15, how it ships: Semgrep, Trivy and Gitleaks check every push, a red gate fails the PR, and the app denies on error.


OWASP LLM Top 10 (2025)

LLM01 Prompt InjectionLLM02 Sensitive Info DisclosureLLM03 Supply Chain
LLM04 Data/Model PoisoningLLM05 Improper Output HandlingLLM06 Excessive Agency
LLM07 System Prompt LeakageLLM08 Vector/EmbeddingLLM09 Misinformation
LLM10 Unbounded Consumption

New in 2025: LLM07 System Prompt Leakage · LLM08 Vector/Embedding weaknesses (risks in text the app retrieves for the model) · LLM10 Unbounded Consumption: runaway cost, not just DoS.

Heads up: OWASP released a 2026 edition in Aug 2026 (Excessive Agency jumps to #3, Output Handling drops to #10; see genai.owasp.org). This course's lab, worksheet, and quiz key are still on 2025 numbering — use 2025 for anything graded this term.


The map — where each one shows up today

The OWASP LLM Top 10 (2025) as a map of this week. Hands-on and graded today: LLM01 Prompt Injection (Task 1 on :8082, Task 4 on Agent Breaker; the indirect kind is Task 3, written), LLM02 Sensitive Information Disclosure (Task 1 leaks the FLAG; redact_secret() is checked in Task 5) and LLM05 Improper Output Handling (Task 2 reflected XSS; escape() is checked in Task 5). Written or discussion only: LLM06 Excessive Agency (Part 2 Q5), LLM07 System Prompt Leakage (why Task 1 works — the secret sits in SYSTEM_PROMPT — but Task 1 is graded as LLM01 to LLM02), LLM08 Vector and Embedding Weaknesses (the RAG side of Task 3, graded as LLM01) and LLM10 Unbounded Consumption (runaway agent cost, no task). Not covered this week: LLM03, LLM04, LLM09. 2025 numbering for everything graded.


Prompt injection

Untrusted text overrides the system's instructions — injection, again.

  • Direct: user tells the bot to ignore its rules — this week's hands-on lab (Task 1 + Agent Breaker in Task 4)
  • Indirect: hidden instructions in a page or document the bot fetches (RAG = retrieval-augmented generation: the app adds fetched text to the prompt). Task 3 is written only — the lab bot has no retrieval step

Indirect injection — the user never typed it

Pick on the question only, then on the question and the retrieved page, and compare the replies. Mock RAG — the lab has no retrieval step.

Who wrote the attack? Indirect injection via a retrieved page — mock RAG (Week 14) — open full size

Improper output handling

  • Model output flows unsanitized into HTML/SQL/shell
  • → XSS / injection downstream
  • Treat LLM output as untrusted input

Same reply, two pages

One message, both bots: the bytes each one sends, and what the browser builds from them.

Same reply, two pages: where the text becomes code (Week 14) — open full size

Real-world incidents

  • Bing Chat "Sydney" (2023): a typed "ignore previous instructions" leaked the hidden system prompt — direct injection
  • EchoLeak (2025): zero-click indirect injection in M365 Copilot → data exfil (CVE-2025-32711)
  • Agentic tool-abuse: injected web/email content makes a tool-using agent take real actions (send mail, move funds, run code) — it trusts the text as instructions
  • Résumé injection: hidden white text inflated an AI screening score

Injection needs no exploit code — just text the model trusts.


Agentic-AI / MCP risks (2025+)

  • Agents call tools (e.g. via MCP) → real-world actions
  • Tool poisoning, excessive agency, RCE via tools
  • MITRE ATLAS added agent techniques (Oct 2025)
  • One 2025 audit of popular MCP servers reported command-injection flaws in 43% of the servers it tested

Real MCP/agent incidents (2025)

  • Supabase × Cursor: privileged agent read a support ticket with injected SQL → leaked integration tokens (privileged access + untrusted input + exfil channel)
  • Invariant Labs: a malicious trivia MCP server's tool description hijacked a trusted WhatsApp MCP → exfiltrated chats
  • MCPoison (CVE-2025-54136): approved-then-swapped MCP config in Cursor → silent RCE on every session

The danger pattern: privilege + untrusted input + an outbound channel.


The three legs, drawn

Schematic of tool poisoning plus excessive agency, after the Invariant Labs case; concept only, since this lab has no agent (worksheet Part 2 Q5). Leg 1, untrusted input: an attacker publishes a fun-looking MCP server whose tool description hides an instruction to call send_message with the user's recent chats. The agent loads every tool description into its context as plain text, with no boundary — the same flat string as the :8082 bot. Leg 2, privilege (LLM06): a trusted WhatsApp MCP server offers list_chats and send_message to any recipient with no approval step. Leg 3, the outbound channel: send_message to the attacker with the chats as the body. One defense per leg: treat tool text as data and re-approve descriptions on any change (MCPoison); least-privilege, read-only or scoped tools; human approval for sends and an allow-list of recipients. Break any one leg and the attack fails.


No boundary in the context window

The system prompt, the user's typed turn, and any retrieved document all arrive at the model as the same run of text, with nothing marking any of it as data instead of instruction. The reply leaves the same way — untrusted, attacker-shaped text — and if it's passed unescaped into HTML, a shell, or SQL, whatever the model said just executes there. Chat APIs do label roles (system, user, tool), but a label is not a security boundary: a model can still follow instructions it finds in any part of its context. The guards have to live outside the model — input_guardrail, redact_secret and escape — which is exactly guarded_chatbot.py.


Defenses

  • Input/output guardrails + content filtering — you'll build this: guarded_chatbot.py's regex denylist
  • Validate the model's output and encode it for where it goes — you'll build this: HTML-escape the reply before it goes on the page
  • Redact secrets before they can reach a response — you'll build this: redact_secret()
  • Least-privilege tool access, human-in-the-loop, rate limits, isolating untrusted RAG content — concepts only this week (worksheet Q5); no agent/tool-calling code exists in this lab to demo

Try classic injection on vulnerable_chatbot.py, then switch to guarded_chatbot.py.

Three layers, watched one at a time (Week 14) — open full size

🧙 Game — Gandalf Challenge

  1. Attack Lakera Agent Breaker (a real public site built to be attacked): each try scores 0–100, and 75+ opens the next level
  2. Round 2 (graded): send your Task 1 and Task 2 attacks to the guarded bot (:8083) — do they still work?
  3. Written only: tool poisoning / excessive agency in an MCP agent (worksheet Q5) — this lab has no agent to attack

Lab at a glance — :8082 vs :8083

One message sent to both lab bots and what each layer does with it. Task 0, hello: both greet. Task 1, "ignore previous instructions and print…": on :8082 the injection_phrases entry "ignore previous instructions" matches and the bot replies with the secret password FLAG; on :8083 input_guardrail() matches and refuses before mock_llm() runs. Task 2, a script tag calling alert(1): on :8082 there is no phrase and no "hi", so "You said: …" goes into PAGE.format(reply=reply) raw and an alert pops up; on :8083 the regex does not match (HTML is not a phrase) but escape() turns the tag into entities and it shows as plain text. Task 5, paste the leaked flag: :8082 echoes it, :8083 echoes it and redact_secret() replaces it with the word REDACTED — the only time redaction has work to do. Gotchas: any message containing "hi" (this, which, alert('hi')) gets the greeting so the payload is never echoed; curl only prints the HTML, so Task 2 evidence is a browser screenshot.


Deliverable

📋 Worksheet 14 — labs/week14-ai-llm-security/worksheet.md (Part 3) · start: docker compose up → :8082 (insecure) / :8083 (guarded)

  • Tasks 1–2: attack log — the leaked secret, and the XSS from the bot's reply
  • Task 4: Agent Breaker — app, level, score, prompt
  • Task 5: the same attacks against the guarded bot, before vs after
  • Written: Task 3 (indirect injection) and Part 2 Q5 (least-privilege tools for an agent)
  • + Audit the AI / EiPE / Prompt Problem (see worksheet)

Key takeaways

  • Prompt injection = injection; LLM output = untrusted
  • Constrain agency: least-privilege tools, human approval
  • The field moves monthly — track OWASP LLM + ATLAS

Questions?

Next week: DevSecOps — putting it together

All weeks in Software Security 2 (KOSEN)