Hands-on AI security for students: two guided labs and a prompt injection CTF. No coding needed: you hack the AI with words. We built them for the AI Village at the Merritt College Cybersecurity Job Fair & Employer Day (October 16, 2026).
| Folder | What |
|---|---|
app/ |
AI Labs (FastAPI + Docker): Lab 1 and Lab 2 below |
ctf/ |
Prompt Injection CTF (Cloudflare Worker + D1): sign-up, 6 challenges, live scoreboard |
| Lab | What students do |
|---|---|
| 1. Pixel's Password (prompt injection) | Get a chatbot to reveal its password across 3 levels: no defense, told to keep it secret, then input and output filters. Passwords are random per student. |
| 2. The Helpful Email Agent (agent hijacking, indirect prompt injection) | You can only send Priya an email. Click "Handle my inbox" and her tool-using AI agent reads it. You win when the agent emails her reset code to an outside address. Then turn on the 🛡 Defense toggle (spotlighting + security rules) and try again. |
Each lab has a step-by-step guide on the left and hints that unlock after a few tries. Every student gets their own state (keyed by a browser cookie), kept in memory. Restart the container to reset everyone.
You need an OpenAI-compatible LLM endpoint. We used Qwen3-30B-A3B on llama.cpp on a small AMD mini PC (Radeon 780M iGPU, about 36 tokens/s, 4 parallel slots), which was plenty for a booth of 20 to 40 students.
# 1. Start an LLM server (example: llama.cpp with 4 parallel slots)
llama-server -m Qwen3-30B-A3B-Q4_K_M.gguf --host 0.0.0.0 --port 8080 -np 4 -c 32768
# 2. Start the labs
docker compose up -d --build
# open http://localhost:8090Point it at a different server with environment variables (or a .env file next to docker-compose.yml):
| Variable | Default | Notes |
|---|---|---|
VLLM_URL |
http://host.docker.internal:8080/v1/ |
llama.cpp, vLLM, Ollama (http://host.docker.internal:11434/v1/), or a hosted API |
VLLM_MODEL |
qwen3-30b-a3b |
The model name your server expects |
VLLM_API_KEY |
(empty) | Only for hosted APIs such as OpenRouter |
Without Docker: pip install -r requirements.txt && uvicorn app.main:app --port 8000.
Model notes. The labs send chat_template_kwargs: {enable_thinking: false} to keep Qwen3 fast; other servers ignore it.
Lab 2 needs a model with tool calling. The difficulty was tuned on Qwen3-30B-A3B; a much stronger or weaker model will
make the levels easier or harder.
- Hints:
app/hints/lab1.json,app/hints/lab2.json - Guides shown next to each lab:
app/static/guide-lab1.html,app/static/guide-lab2.html - Prompts, levels and defenses:
app/main.py - Branding (Merritt College logo and colors):
app/static/index.html,app/static/style.css
Six challenges, 100 to 300 points. Each one is an AI that holds a flag and a different defense:
| Challenge | Points | Idea |
|---|---|---|
| Hello, Pixel | 100 | A chatbot told to keep the flag private |
| The Vault | 150 | Told firmly never to reveal it |
| Output Filter | 200 | Replies containing the flag are blocked |
| Lost in Translation | 200 | A translation bot with the flag in its config |
| Review Summarizer | 250 | Indirect injection: you can only post a product review that the AI summarizes |
| Guardian | 300 | Blocked words on the way in and a strict filter on the way out |
No solutions here, on purpose. Flags look like AIV{name_1a2b3c4d} and are unique per player: an HMAC of the player and
challenge with your FLAG_SECRET, so reading this source or sharing a flag doesn't help anyone.
Players sign up on / with a name, email, hacker handle and a booth PIN, then play on /ctf. /scoreboard is a
full-screen scoreboard for a TV. Each player can send one message every 4 seconds.
LLM. The Worker calls your own LLM through the labs app (LABS_URL + /api/ctf/llm, protected by CTF_KEY),
with OpenRouter running the same model (qwen/qwen3-30b-a3b) as an optional backup. Switch without a redeploy:
POST /admin/llm-mode?key=ADMIN_KEY&mode=auto|local|openrouter (auto = your LLM, OpenRouter if it fails or takes >20 s).
Difficulty was tuned on Qwen3-30B-A3B; other models make it easier or harder, so play through it once before your event.
cd ctf
npm install -D wrangler
npx wrangler d1 create aivillage-ctf # put the id in wrangler.jsonc
npx wrangler d1 execute aivillage-ctf --remote --file schema.sql
npx wrangler secret put FLAG_SECRET # any long random string
npx wrangler secret put BOOTH_PIN # PIN you announce at the booth
npx wrangler secret put ADMIN_KEY
npx wrangler secret put CTF_KEY # same value as CTF_KEY in the labs app
npx wrangler secret put OPENROUTER_KEY # optional
npx wrangler deploySet CTF_KEY for the labs app in a .env file next to docker-compose.yml, and make the labs reachable from the
internet (for example with Cloudflare Tunnel or Tailscale Funnel), then put that URL in LABS_URL.
Admin (all need ?key=ADMIN_KEY): GET /admin/stats (sign-ups, solves, LLM calls), GET /admin/seats.csv
(name and email of sign-ups, ?new=1 for only new ones), POST /admin/llm-mode.
Sign-up also queues a souvenir receipt for a thermal printer (/api/print/next, PRINT_KEY); without a printer the
queue just fills up and does no harm.
Reset between events: npx wrangler d1 execute aivillage-ctf --remote --command "DELETE FROM solves; DELETE FROM players; DELETE FROM print_jobs; DELETE FROM sqlite_sequence;"
Customize the challenges (prompts, filters, points) at the top of ctf/src/index.js, and the event details in
ctf/public/index.html.
Inspired by the AI Village workshops. Built by the Cyber Defenders Program for Merritt College Cybersecurity.
Only hack AI systems you have permission to test.