A SQL injection example is worth more than a paragraph of theory: it happens whenever an application builds a SQL query by concatenating untrusted input, letting an attacker change what the query does instead of just what it looks for. SQL injection has topped web-vulnerability lists for over two decades, and it remains common because the mistake, gluing user input into a query string, is easy to make and easy to miss. This guide walks through a concrete example of SQL injection, covers the main attack types conceptually, and then focuses on the fixes. Developers searching how to do a SQL injection attack almost always want to understand the mechanism well enough to defend against it, so that is where the emphasis goes. There are no working exploit payloads against real systems here.
The core SQL injection example
Consider a login lookup that builds its query by string concatenation:
# Vulnerable: user input concatenated directly into SQL
username = request.form['username']
query = "SELECT * FROM users WHERE username = '" + username + "'"
cursor.execute(query)
The developer intended username to be data, a value to match. But it lands inside the query text, where the database cannot tell the difference between the data and the surrounding SQL. If an attacker supplies input containing a single quote, they close the string literal early and everything after it is interpreted as SQL rather than as a value. A classic illustrative input like ' OR '1'='1 turns the WHERE clause into a condition that is always true, so the query returns rows it should not.
That is the whole vulnerability. The database faithfully executes the query it was handed; the flaw is that the attacker got to help write it. Every SQL injection attack example, no matter how elaborate, is a variation on this one idea.
SQL injection attack types
Understanding the SQL injection attack types helps you recognize them in the wild and test for them:
- In-band (classic). The results come back through the same channel used to inject, either as returned data or as an error message that leaks information. The example above is in-band.
- Error-based. The attacker deliberately provokes database errors and reads schema or data out of the error text. This is why leaking raw database errors to users is dangerous.
- Union-based. The attacker appends a
UNION SELECTto graft data from other tables onto the legitimate result set. - Blind (boolean). The app returns no data or errors, so the attacker infers information one bit at a time by observing whether the page behaves differently for true versus false conditions.
- Blind (time-based). When even boolean differences are not visible, the attacker uses conditional delays and measures response time to extract data slowly.
- Second-order. Malicious input is stored safely, then later used unsafely by a different part of the application. The injection fires when the stored value is concatenated into a query elsewhere, which is why input sanitization alone is not a reliable fix.
You do not need working payloads for each; recognizing that these categories exist tells you where to look and why "we escape quotes on input" is not enough.
Why the vulnerability happens
The root cause is mixing code and data. When query structure and user-supplied values share the same string, the boundary between them is set by punctuation the attacker can supply. Escaping tries to neutralize that punctuation, but escaping is error-prone: different databases have different rules, different contexts (string, numeric, identifier) need different handling, and second-order flaws sidestep input-time escaping entirely. The reliable fix removes the mixing rather than trying to sanitize it.
The fix: parameterized queries
Parameterized queries (prepared statements) send the query structure and the values to the database separately. The database compiles the query with placeholders, then binds the values as pure data that can never be reinterpreted as SQL. The always-true trick simply becomes a search for a username that literally contains those characters, which matches nothing.
# Safe: structure and data sent separately
query = "SELECT * FROM users WHERE username = %s"
cursor.execute(query, (username,))
The same pattern applies across stacks:
// Java: PreparedStatement binds the value as data
PreparedStatement ps =
conn.prepareStatement("SELECT * FROM users WHERE username = ?");
ps.setString(1, username);
ResultSet rs = ps.executeQuery();
Use parameterization everywhere, including for values that "come from a dropdown" or "are always numeric." Those assumptions break, and the cost of parameterizing is near zero.
Defense in depth around the fix
Parameterized queries eliminate the core flaw, but a resilient application layers additional controls:
- Least-privilege database accounts. The account your app uses should not be able to drop tables or read tables it does not need. If an injection does slip through somewhere, the damage is bounded.
- Input validation as a complement, not a substitute. Validate type, length, and format because it is good hygiene and catches mistakes, but never rely on it as your only defense against injection.
- Do not leak database errors. Return generic errors to users and log the details server-side, to shut down error-based and reconnaissance techniques.
- Use an ORM or query builder carefully. These parameterize by default, but raw-query escape hatches reintroduce the risk. Audit every place your code drops to raw SQL.
- Stored procedures help only if they themselves use parameterization internally; a procedure that concatenates input is just as vulnerable.
Finding SQL injection before attackers do
A SQL injection attack tutorial teaches you the pattern; scanning finds it across a real codebase. Two complementary approaches:
- Static analysis (SAST) traces user input from request handlers to query execution and flags concatenation into a SQL sink. This catches the flaw in your own code at review time.
- Dynamic testing (DAST) exercises the running application, sending safe probe inputs to endpoints and observing whether the application behaves in ways that indicate an injection path. A DAST scanner can crawl authenticated flows and surface injectable parameters that a code review might miss, especially in older or generated code.
For teams, both belong in CI so a newly introduced concatenated query fails the build rather than reaching production. If you are training developers to recognize and avoid these patterns, the Safeguard Academy has hands-on material, and our writeup on data-flow analysis covers how the underlying tainted-data tracking works.
FAQ
What is a simple SQL injection example?
A login query built by concatenation, such as "SELECT * FROM users WHERE username = '" + input + "'", is the classic example. Supplying input that closes the quote early lets the attacker inject SQL, for instance an always-true condition that returns rows the query should not. The fix is a parameterized query that sends the value as data.
What are the main SQL injection attack types?
The main types are in-band (classic and error-based), union-based, and blind (boolean and time-based, where the attacker infers data from application behavior or response timing). There is also second-order injection, where input stored safely is later used unsafely in a query elsewhere.
Do parameterized queries fully prevent SQL injection?
Parameterized queries eliminate the core cause by separating query structure from data, so they prevent injection anywhere they are used consistently. The residual risk is code paths that drop to raw concatenated SQL, so audit every raw-query escape hatch and pair parameterization with least-privilege database accounts.
How do I test my application for SQL injection?
Use static analysis to trace user input into query sinks in your source, and dynamic application security testing to probe the running app's endpoints for injectable parameters. Run both in CI so newly introduced injection paths fail the build, and validate fixes rather than assuming a scanner caught everything.