How does SQL injection actually work, and how do parameterized queries stop it?
SQL injection happens the instant untrusted input is allowed to change the shape of a query, not just its values — parameterized queries fix that by construction.
LEARN BY DOINGPut your intuition to work3 steps
How does SQL injection actually work, and how do parameterized queries stop it?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#The core mechanism: query text vs. query data
A SQL query has two fundamentally different kinds of content: the grammar (SELECT, WHERE, AND, table and column names) and the data (the actual values being compared or inserted). SQL injection happens the instant those two get mixed into the same string before the database ever sees them. If a value coming from user input can change where a clause ends, add a new clause, or comment out part of the query, the attacker isn't just supplying data anymore — they're co-authoring the SQL your application runs.
#The vulnerable pattern: string concatenation
# VULNERABLE — user input concatenated directly into SQL textdef find_user(cursor, username, password):query = f"""SELECT id, username, is_adminFROM usersWHERE username = '{username}' AND password_hash = '{password}'"""cursor.execute(query)return cursor.fetchone()
This looks completely reasonable until username contains a single quote. Send username = "admin' --" with any password at all, and the query the database actually executes is:
SELECT id, username, is_adminFROM usersWHERE username = 'admin' --' AND password_hash = 'anything'
-- starts a SQL comment, so everything after it — including the password check — is discarded before the database even parses it. The attacker is authenticated as admin without ever knowing the password. A different payload, ' OR '1'='1, turns the WHERE clause into a tautology that matches every row, which is exactly as dangerous against a query that wasn't even about login (bulk data exfiltration from any endpoint that filters by user-supplied text).
#The fix: parameterized queries
# FIXED — the query's shape is fixed; input travels as data, never as SQL textdef find_user(cursor, username, password_hash):query = """SELECT id, username, is_adminFROM usersWHERE username = %s AND password_hash = %s"""cursor.execute(query, (username, password_hash))return cursor.fetchone()
The mechanism here isn't "the driver escapes quotes better" — it's structurally different. The database driver sends the query template and the parameter values to the database as two separate pieces over the wire protocol. The database compiles the template — deciding what the grammar is — before it ever sees the values, then binds the values into the already-fixed plan purely as data. A value like admin' -- can never become part of the grammar, no matter what characters it contains, because by the time the database sees it, the grammar has already been decided.
#What parameterization does not cover
Placeholders work for values — things that go where a literal would go. They don't work for identifiers — table names, column names, ORDER BY direction (ASC/DESC) — because those are part of the grammar itself, not a value being compared. If you need to build a query with a dynamic column or table name, you must validate it against a fixed allowlist of known-safe names in your own code; you cannot parameterize your way out of that case.
#Beyond the classic case
| Variant | How it manifests | Why parameterized queries still stop it |
|---|---|---|
| Classic / in-band | Injected clause changes results or bypasses auth (' OR '1'='1) | Input is bound as a literal value and can never become part of the grammar |
| Blind boolean-based | Attacker infers data bit by bit from true/false differences in app behavior, no visible error needed | Same fix — the payload never reaches the parser as SQL either way |
| Blind time-based | Attacker injects something like ... OR SLEEP(5) and measures response latency | Same fix — SLEEP(5) stays a harmless string literal, never executed as SQL |
| Second-order injection | Malicious input is stored safely, then later read back and concatenated unsafely into a different query | Parameterize every query that touches that data, not just the one that first wrote it |
🧠 Check your understanding
0/3 · 0/3 answered1. A login query is built with a Python f-string: WHERE username = '{username}' AND password = '{password}'. An attacker submits username = admin' --. What happens?
2. Why do parameterized queries prevent SQL injection, structurally, rather than just 'escaping dangerous characters better'?
3. Why can't table or column names be supplied through a parameterized placeholder?