A good SQL injection lab has learners exploit a vulnerable query, then fix it with a parameterized query, then deal with the one case parameters cannot cover: dynamic table names, column names and sort order. Finish by showing why escaping is a weak fix.
Set the lesson up
Give learners a small search endpoint that builds its query by joining strings. Ask them to read another user's row. Most pairs get there within ten minutes, and the exploit makes the next step feel earned.
State the model in one line: the database should never be able to confuse data with commands.
Step one: the primary fix
The OWASP SQL Injection Prevention Cheat Sheet lists four defenses. The first is prepared statements with parameterized queries. Have learners rewrite the vulnerable function so that user input is passed as a bound parameter and never joined into the query text. Then rerun the exploit and watch it fail.
Ask for a test that encodes the exploit string and asserts that no extra rows come back. A passing test is the proof.
Step two: stored procedures are not magic
The cheat sheet says properly constructed stored procedures can be as effective as prepared statements. It also warns that they can raise risk in some setups, because web apps may need broader database roles to execute them, and that dynamic SQL inside a stored procedure should be avoided. The OWASP Top 10 injection page adds that stored procedures can still be vulnerable if they concatenate user data into queries. Discuss this in the debrief, since many learners assume "stored procedure" means "safe."
Step three: the case parameters cannot cover
Add a requirement: let users sort results by a column they choose. Bind variables do not work for column names. The cheat sheet recommends allow-list input validation here, ideally with values coming from the code and user input mapped to expected names. Let learners implement a small map from a few permitted sort keys to fixed column names, rejecting anything else.
Step four: why not just escape
The cheat sheet strongly discourages escaping all user-supplied input. It is database-specific and described as fragile compared to other defenses, and the authors say they cannot guarantee it prevents all SQL injection. Show a payload that defeats a naive filter, so the point lands with evidence.
Debrief questions
- Where in our own services is a query built by joining strings?
- Does our ORM ever let us drop to raw SQL, and who reviews that?
- Which database role does each service use, and does it need that much power?
Follow-up
Send the three questions to each team lead and ask for a short search of their repositories. Static analysis can help find string-built queries, and the OWASP page recommends SAST, DAST and IAST in the pipeline.
See also writing a lab exercise that teaches.
Quick check
Is input validation enough to stop SQL injection?
Not by itself. Use parameterized queries as the primary defense and keep validation as a secondary layer, with an allow-list for structural parts of a query.
Sources
Last reviewed: 19 October 2026.