A SQL injection demo makes one thing obvious very fast: the vulnerability exists the moment your code builds a query by gluing user input directly into a SQL string. The fix is equally simple to state — separate the query structure from the data using parameterized statements — but the reason it works is worth understanding, so this demo walks through the mechanism defensively rather than handing out attack payloads.
We will look at vulnerable code, explain conceptually why an attacker can subvert it, and then show the corrected version. The goal is detection and remediation, not building an exploit against anyone else's system.
The vulnerable pattern
Here is a login lookup that concatenates user input straight into the query. This is the anti-pattern every SQL injection demo starts with:
# VULNERABLE - do not ship this
def find_user(username):
query = "SELECT * FROM users WHERE username = '" + username + "'"
return db.execute(query)
When username is a normal value like alice, the query reads SELECT * FROM users WHERE username = 'alice' and works fine. The problem is that the database receives one flat string. It has no way to know which parts came from your code and which parts came from the user. If the input contains SQL syntax — a quote, a comment marker, a boolean clause — the database parses it as part of the command.
That is the whole vulnerability in one sentence: the data and the code travel together, so the data can become code.
Why it works, conceptually
Imagine an input that closes the string quote early and appends an always-true condition. The database, seeing a single well-formed statement, happily evaluates the whole thing — including the attacker's added logic. Depending on the query, that can mean returning rows the user should never see, bypassing an authentication check, or in worse cases reading or modifying data across tables.
The classic symptom in a real assessment is that appending a single quote to an input field produces a database error. That error leaking back to the user is itself a finding: it tells an attacker the input reaches the query unescaped and reveals the backend.
We deliberately stop at the concept here. You do not need a working payload to fix the bug, and reproducing one against a system you do not own is not something this guide helps with.
The fix: parameterized queries
The correct approach sends the query structure and the data to the database as separate things. The database compiles the query with placeholders first, then binds the user data into those placeholders as pure values that can never be reinterpreted as SQL.
# SAFE - parameterized query
def find_user(username):
query = "SELECT * FROM users WHERE username = %s"
return db.execute(query, (username,))
Now no matter what the user types — quotes, semicolons, whole SQL statements — it is treated as a literal string value for the username column. The structure of the query is fixed before the data ever arrives. This single change eliminates the entire class of injection for that query.
Most languages and frameworks have the same facility:
// Java - PreparedStatement
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE username = ?");
ps.setString(1, username);
ResultSet rs = ps.executeQuery();
What about ORMs and query builders?
ORMs like Hibernate, SQLAlchemy, and ActiveRecord use parameterized queries under the hood, which is why they are safe by default. The trap is the escape hatch: raw-SQL methods where you interpolate strings yourself. session.execute(f"SELECT ... {user_input}") reintroduces the exact vulnerability the ORM was protecting you from. Treat every raw-query call site as something to review carefully.
Dynamic table or column names are the one case parameterization does not cover, because placeholders bind values, not identifiers. There, use a strict allowlist of permitted names rather than passing user input through.
Defense in depth
Parameterization is the fix, but layer these on top:
- Least privilege on the database account. The app's database user should not be able to drop tables or read other schemas. If injection ever succeeds, this limits the blast radius.
- Input validation as a secondary control — reject obviously malformed input early, but never rely on it as the primary defense. Validation catches noise; parameterization catches attacks.
- Suppress detailed database errors from reaching users. Log them server-side instead.
- A web application firewall as a backstop, understanding it can be bypassed and is not a substitute for fixing the code.
Finding injection before attackers do
You can catch these systematically. Dynamic testing tools crawl a running application and probe inputs for injection behavior; a DAST scanner automates exactly this kind of probing across every form and endpoint. Static analysis flags concatenated-query patterns in source before they ship. And code review focused on query construction remains one of the highest-value habits a team can build. Our security academy has a full module on injection classes if you want to go deeper.
FAQ
What is the single most effective defense against SQL injection?
Parameterized queries, also called prepared statements. They separate query structure from user data so input can never be reinterpreted as SQL commands. Nothing else in the defense stack matches this, and it covers the entire class of injection for the queries you apply it to.
Do ORMs make me immune to SQL injection?
Mostly, because ORMs parameterize by default. The exception is raw-SQL methods where you build query strings manually. Those reintroduce the vulnerability, so audit every raw-query call site even in an ORM-based codebase.
Can input validation alone stop SQL injection?
No. Validation is a useful secondary layer but a poor primary defense — clever encodings and edge cases slip past filters. Always use parameterized queries as the real fix and treat validation as reinforcement.
How do I test my own application for SQL injection?
Use a DAST scanner to probe running endpoints, run static analysis to flag concatenated queries in source, and review query-building code manually. Only test systems you own or have written authorization to assess.