Recognize common web risks
Keep data separate from code: parameterized queries, output encoding and allowlists.
- Explain why mixing untrusted data into code causes injection
- Use parameterized queries and allowlists
- Encode output for the context it appears in
Web applications constantly cross trust boundaries: text typed in a browser travels to a server, into a database query, back into an HTML page. The root of a whole family of bugs - injection (OWASP A05:2025) and cross-site scripting - is the same: untrusted data gets treated as code.
If a program builds a database query by gluing user input into the SQL text, a cleverly chosen input can change the query’s meaning, not just its values. The fix is to never mix the two: send the query and the values separately.
1# Unsafe: the input becomes part of the SQL text
2cursor.execute("SELECT * FROM users WHERE name = '" + name + "'")
3
4# Safe: the query and the value travel separately
5cursor.execute("SELECT * FROM users WHERE name = ?", (name,))With a parameterized query (also called a prepared statement), the database receives the SQL with placeholders (? or %s or :name) and the values on their own. A value can contain quotes, keywords or anything else - it will only ever be compared as data. Every mainstream database library supports this; ORMs do it for you.
Some parts of a query can’t be parameters - table names, column names, sort direction. For those, use an allowlist: accept only values from a fixed list you wrote, and fall back to a safe default otherwise.
Practice: attack a login form
Try it
Up to 15 XPBreak into a simulated app with SQL injection
Watch the query change as you type. First, sign in normally with username ana and password blue-kettle-42. Then try a single quote in the username and see what the error tells an attacker.
Now complete the goals. Use the hints if you’re stuck.
A simulation: no real database or network is involved.
SELECT * FROM users WHERE username = '' AND password = ''
- Sign in as admin without the password
- Make the query match every user
- Turn on parameters and watch an attack fail
Try it
Up to 15 XPSafe or unsafe?
Decide whether each snippet keeps untrusted data separate from code.
“cursor.execute("SELECT * FROM users WHERE email = %s", (email,))”
“cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")”
“order = sort if sort in {"name", "price"} else "name"”
“os.system("convert " + uploaded_filename + " out.png")”
“subprocess.run(["convert", uploaded_filename, "out.png"])”
“element.innerHTML = "Hello " + username”
Encoding output
The same idea applies when showing data in a page: to display a comment as text, characters that mean something in HTML (<, >, &, quotes) must be converted to entities like <. Templating engines such as Jinja, React and most others do this automatically - the danger comes from switching it off (|safe, dangerouslySetInnerHTML, innerHTML).
Key takeaways
Injection happens when untrusted data is interpreted as code.
Use parameterized queries, argument lists and allowlists to keep code and data apart.
Encode output for its context; keep the framework’s auto-escaping on.
Lesson quiz
7 questions · pass with 5 correct · up to 50 XP
Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.
Practice: write Python
Write Python in the editor and run it against sample inputs. Python runs locally in your browser using a WebAssembly runtime.
Allowlist a sort column
Column names can’t be query parameters. Each input line is column direction requested by a user. Print ORDER BY COLUMN DIRECTION where the column must be one of name, price, created_at (otherwise use created_at) and the direction ASC or DESC, case-insensitive (otherwise DESC).
- Valid and invalid requests
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Questions about this lesson
Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.
Loading posts…