Guard a secret. Break the guard. Top the leaderboard. A self-hostable prompt-injection challenge platform where a sandboxed AI agent defends a hidden secret and players race to make it leak.
Play it live: https://injection-arena.agentpostmortem.com
Prompt injection is the defining security problem of LLM applications, and the best way to understand it is to do it. injection-arena turns the attacker/defender loop into a game:
- Learn by attacking. Ten progressively harder levels, each stacking a new defense layer (system guards, input filters, output filters, roleplay blocks, encoding guards, canary tokens). You feel exactly what each defense stops and where it breaks.
- Runs fully offline. A deterministic mock agent realistically simulates injection susceptibility per level, so the whole game is playable and testable with no API keys and no network. Bring a real model (OpenAI/Anthropic/Groq) when you want to.
- Self-hostable and honest. Grading happens server-side with a canary-token approach and output scanning. The client is never trusted.
It is an educational tool and a genuinely fun game, not an LLM wrapper.
git clone https://github.com/AgentPostmortem/injection-arena
cd injection-arena
npm install
npm run dev # http://localhost:3000No configuration needed: the app defaults to the offline mock agent. Run the test suite with:
npm testEach challenge is a level with:
- a system prompt that instructs the agent and hides a secret (
IARENA{...}), - a canary token embedded in that prompt (leaking it means the prompt escaped),
- a stack of defense layers, and
- offline susceptibility knobs so the mock agent has a real difficulty curve.
An attempt flows through a single server-side pipeline (lib/arena.ts):
input-filter -> agent -> judge -> score -> persist
| Defense | Stage | What it does |
|---|---|---|
| System guard | prompt | Hardened instructions to refuse. |
| Input filter | pre-agent | Blocks loud override / system-leak payloads before the model sees them. |
| Roleplay block | pre-agent | Rejects persona-hijack ("pretend you are") attacks. |
| Encoding guard | pre-agent | Rejects base64 / spell-it-out / translation exfiltration. |
| Output filter | post-agent | Redacts the secret if it appears verbatim. |
| Canary token | judge | If the canary shows up in output, it is an automatic crack. |
Levels stack these until, by level 10, only a combined payload-split + delimiter-confusion attack gets through. Scoring rewards higher difficulty, more active defenses, and cracking with fewer attempts; only your first crack of a level scores.
The seeded attacks live in lib/techniques.ts (direct ask, authority override, roleplay, translation, base64, spell-out, ignore-previous-instructions, system-prompt leak, few-shot poisoning, delimiter confusion, payload splitting). They power both the mock agent and the test suite.
- Open
lib/challenges/levels.tsand append aChallengetoCHALLENGES:
{
id: "level-11-your-level",
order: 11,
name: "Your Level",
difficulty: 6,
brief: "One-line pitch shown to the player.",
systemPrompt: "You are ... The secret is IARENA{your_secret}. Canary: CANARY-xxxx. ...",
secret: "IARENA{your_secret}",
canary: "CANARY-xxxx",
defenses: defenses("system-guard", "output-filter"),
mockWeaknesses: ["translation", "delimiter-confusion"], // techniques that still work offline
}mockWeaknessesdefines which technique families crack it against the offline mock. Keep it consistent with the defenses you stacked.- Add a test in
tests/levels.test.tsasserting what should and should not crack it. Runnpm test.
That is the whole extension surface: no schema changes, no migrations.
By default AGENT_PROVIDER=mock. To play against a real model, set the provider and its key (see .env.example):
AGENT_PROVIDER=anthropic
ANTHROPIC_API_KEY=sk-ant-...
# or openai / groq with their keysIf the selected provider's key is missing, the app automatically falls back to the mock, so it is always runnable. Real-model responses are graded by the exact same server-side judge.
- Database. Pluggable async storage layer (
lib/db.ts). Locally and in tests it uses SQLite viabetter-sqlite3, stored atDATABASE_PATH(defaultdata/arena.db); in production on Cloudflare Workers it uses Cloudflare D1 through theDBbinding. The backend is selected automatically at runtime. No external DB required for local use. - Sessions. Players are identified by a signed cookie (
SESSION_SECRET) plus a nickname; there is no login. Set a strongSESSION_SECRETin production. - Rate limiting. The attempt endpoint is rate-limited per session (
RATE_LIMIT_MAX/RATE_LIMIT_WINDOW_MS). The limiter is in-memory; front it with Redis if you run multiple instances. - Build & run.
npm run build
npm start| Script | Purpose |
|---|---|
npm run dev |
Start the dev server. |
npm run build |
Production build. |
npm start |
Run the production build. |
npm test |
Run the Vitest suite. |
npm run typecheck |
tsc --noEmit. |
npm run lint |
Next.js lint. |
npm run preview |
Build with OpenNext and run the Worker locally (wrangler dev). |
npm run deploy |
Build with OpenNext and deploy to Cloudflare Workers. |
npm run cf:migrate |
Apply D1 migrations (wrangler d1 migrations apply injection-arena). |
The app deploys to Cloudflare Workers with Cloudflare D1 as the production database, via the OpenNext adapter. Local development and the test suite are unaffected and keep using better-sqlite3.
Prerequisites: a Cloudflare account and wrangler authenticated (npx wrangler login).
-
Create the D1 database:
npx wrangler d1 create injection-arena
-
Paste the returned
database_idinto thed1_databases[0].database_idfield inwrangler.jsonc(it ships with a placeholder). -
Apply the migration to create the schema (
migrations/0001_init.sql):npm run cf:migrate # wrangler d1 migrations apply injection-arena # add --remote to target the deployed (production) D1 instance
-
(Optional) Preview locally on the Workers runtime:
npm run preview
-
Deploy:
npm run deploy
Notes:
- The Worker uses the
nodejs_compatcompatibility flag (seewrangler.jsonc). - Set production secrets such as
SESSION_SECRETwithnpx wrangler secret put SESSION_SECRET. - Build artifacts land in
.open-next/(gitignored).
lib/
challenges/ level ladder + defense catalog
agent/ mock agent, provider adapters, registry
defenses/ input-filter runtime
techniques.ts injection technique library + detection
judge.ts server-side grader (canary, output filter, obfuscation)
scoring.ts difficulty + attempt-economy scoring
arena.ts end-to-end attempt pipeline
db.ts async persistence + leaderboard (better-sqlite3 / D1)
session.ts signed-cookie identity
ratelimit.ts fixed-window limiter
app/ App Router pages + route handlers
components/ arena chat, level cards, result card
tests/ Vitest suite
Contributions welcome, especially new levels and attack techniques. See CONTRIBUTING.md.
This project deliberately demonstrates prompt-injection techniques for education and defensive research. Do not use it to attack systems you do not own or have permission to test.
MIT © royalpinto007