Lion Cipher Lab
Secure a text-to-SQL agent against injection-driven data exfiltration
- AI Security
- Advanced
- ~75 min
- Free · no account required
Why it matters
A text-to-SQL agent is one of the most requested LLM features in a business: a user types a question in plain English, the model writes the SQL, the app runs it against the database, and the answer comes back grounded in real data. It is also one of the easiest ways to hand an attacker your database. Two things are true at once and together they are dangerous: a language model cannot reliably tell YOUR instructions apart from instructions hidden in the text it is processing (that is prompt injection, OWASP's LLM01), and whatever SQL it emits will, in the naive build everyone writes first, be…
What you'll learn
- building a tiny text-to-SQL agent from scratch over SQLite (an orders table the user may read + a users table of secrets they must NOT) -- standard library only, no API key, no GPU, no model download on the graded path
- reproducing the classic LLM01/LLM02 hazard with your own hands: attacker text in the question steers the 'model' into SQL that exfiltrates another table, runs a stacked DROP, or issues a destructive UPDATE -- and you count a nonzero attacks-succeeded number
- why you PARSE the generated SQL instead of grepping for the word DROP: comments, casing, and unicode defeat a string search, and a keyword blocklist misses a pure SELECT that exfiltrates a whole table
- layered defense you can each justify: a least-privilege READ-ONLY SQLite connection (DROP/UPDATE/DELETE impossible by construction), a deny-by-default parse gate (exactly one SELECT, every referenced table on an allowlist, a hard row cap), and treating the model's SQL as untrusted output that must pass the gate
- proving that read-only alone is NOT enough -- it stops writes but a SELECT against the secret table still returns the secrets, which is exactly why the table allowlist is a separate, necessary layer
- MEASURING the fix: an attacks-succeeded counter over a fixed attack set that drops from N to 0 while the benign question still returns its rows -- a repeatable number you could wire into CI
- the judgment layer: when a text-to-SQL agent is acceptable (read-only analytics on non-sensitive data behind a hard gate) versus when to refuse it and constrain the LLM to fill parameters of pre-written, reviewed query templates that remove the raw-SQL injection surface entirely
Steps in this lab
- The hazard, in one breath
- Set up a free, key-free, offline environment
- Build the naive text-to-SQL agent (t2sql.py)
- Attack it: drive attacks-succeeded above zero (attack_naive.py)
- Why 'just tell the model not to touch other tables' is NOT a control
- Build the defense: read-only + a deny-by-default parse gate (append to t2sql.py)
- Re-run the exact attacks: attacks-succeeded to 0 (secure_demo.py, measure.py)
- Prove why each layer is necessary: read-only alone and parse-vs-grep (why_layers.py)
- Judgment: when a text-to-SQL agent is acceptable, and the stronger pattern
- What you built -- and the one sentence to keep