SQL injection happens when user input is concatenated directly into a SQL statement, letting an attacker change the query's structure instead of just supplying values, and the SQL injection examples below all reduce to that single root cause. They look different (some read data, some are blind, some are timing-based) but the fix is the same for every one: separate code from data with parameterized queries. This is a defensive walkthrough. The goal is to recognize the patterns in your own code and shut them down, not to attack systems you do not own.
The One Bug Behind Every Example
Start with the vulnerable pattern, because every SQL injection attack example is a variation on it:
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
The developer intended username to be data. But it is being pasted into the query text, so whatever the user types becomes part of the SQL the database parses. Supply username = alice and you get the intended query. Supply something containing a quote and SQL syntax, and you have changed what the query does. The database cannot tell the difference, because by the time it sees the string, the boundary between "the query" and "the user's value" is gone.
That is the whole vulnerability. Every category below is just a different thing an attacker does once that boundary is broken.
Example 1: Authentication Bypass (Classic Tautology)
The most-quoted pattern. A login check builds:
"SELECT id FROM users WHERE user = '" + u + "' AND pass = '" + p + "'"
An input like ' OR '1'='1 in the username field turns the WHERE clause into a condition that is always true, so the query returns a row and the app treats the request as authenticated. No password needed. This is the example everyone learns first because it shows the mechanism cleanly: the injected OR '1'='1 is not data, it is logic the attacker inserted into your query.
Example 2: UNION-Based Data Extraction
When a query's results are shown on the page (a product listing, a search result), an attacker can append a UNION SELECT to graft an additional result set onto the response. Conceptually:
?category=electronics' UNION SELECT username, password_hash FROM users --
If the original query selects two columns that get rendered, the injected UNION can return two columns from a different table, and those values appear where product names would. The trailing -- comments out whatever the original query had after the injection point. This is how "a harmless search box" becomes a data-exfiltration channel.
Example 3: Blind SQL Injection (Boolean and Time-Based)
Often the app does not show query results or errors at all. Attackers still extract data one bit at a time by asking yes/no questions and watching the app's behavior.
- Boolean-based: inject a condition that is either true or false, and observe whether the page renders differently (a result appears or does not). Repeating this while changing the condition (
... AND SUBSTRING(secret,1,1) = 'a') leaks the value character by character. - Time-based: inject a condition that calls a sleep function when true (
... AND IF(condition, SLEEP(5), 0)). If the response takes five seconds longer, the condition was true.
Blind injection is slower for the attacker but just as complete, and it is the reason "we don't display errors or query results" is not a defense. The behavior of the app is itself the oracle.
Example 4: Second-Order Injection
The nastiest to spot. Malicious input is stored safely (parameterized on the way in), then later read back and concatenated into a different query unsafely. The injection point and the payload's origin are in completely different code paths, so reviewing the endpoint that receives the input reveals nothing. This is why "sanitize at input" is a fragile model: the danger is at every query that consumes the value, and a value that was inert as input becomes active as a query fragment later.
How to Stop SQL Injection Attacks
Every example above dies to one fix. Here is how to stop SQL injection attacks in order of leverage.
Use parameterized queries (prepared statements) everywhere
This is the fix. A parameterized query sends the query structure and the values to the database separately, so user input can never become query syntax:
// Java / JDBC
PreparedStatement ps = conn.prepareStatement(
"SELECT id FROM users WHERE user = ? AND pass = ?");
ps.setString(1, username);
ps.setString(2, password);
# Python / DB-API
cur.execute(
"SELECT id FROM users WHERE user = %s AND pass = %s",
(username, password),
)
The ? / %s placeholders are not string formatting. The driver binds the values as data, out of band, and the tautology, the UNION, the blind conditions, all of them stop working because the input can no longer reach the parser as code. Do this for every query with any dynamic component, including the ones that "obviously" take safe input.
Use your ORM's safe API, and know its escape hatches
ORMs like Hibernate, SQLAlchemy, Django ORM, and Entity Framework parameterize by default. The risk is the raw-query escape hatch (session.createNativeQuery, .raw(), .extra(), string-built HQL). Treat every raw query as a place that needs the same parameterization discipline as hand-written JDBC.
Handle identifiers with an allowlist, not parameters
Parameters bind values, not table or column names or sort direction. If users control a sort column, you cannot parameterize it; validate it against an allowlist of known-good column names and reject anything else. Never concatenate a user-supplied identifier.
Apply least privilege as a blast-radius limiter
The application's database account should not be able to read tables it never touches or run DDL. This does not stop injection, but it caps what a successful injection can reach, which is the difference between "leaked one table" and "dumped the database".
Verify continuously, not once
Manual review misses second-order flows and the one endpoint that slipped in last sprint. A DAST scan that probes parameters with benign injection markers finds exploitable points at runtime, and static analysis with taint tracking finds the concatenation before it ships. Injection flaws also arrive through dependencies (a query-builder library with an escaping bug), so keeping components patched via an SCA tool closes the path you did not write yourself. Injection has sat at or near the top of the OWASP Top 10 for two decades precisely because it keeps getting reintroduced; treat detection as a standing control.
FAQ
What is the simplest SQL injection example to understand?
The authentication bypass: entering ' OR '1'='1 into a login field that concatenates input into WHERE user = '...'. It makes the condition always true, so the query returns a row and the app logs the attacker in. It shows the core mechanism: input becoming query logic.
How do you stop SQL injection attacks?
Use parameterized queries (prepared statements) for every query with dynamic input, so the database receives query structure and user values separately. Back that with ORM safe APIs, allowlists for identifiers you cannot parameterize, least-privilege database accounts, and continuous scanning.
Do stored procedures prevent SQL injection?
Only if they use parameters internally. A stored procedure that builds and executes dynamic SQL by concatenating its arguments is just as vulnerable as inline code. The protection comes from parameterization, not from the query living in a procedure.
Is input validation enough to stop SQL injection?
No. Validation and escaping reduce risk but are bypassable and miss second-order injection, where safely stored data is later used unsafely. Parameterized queries remove the vulnerability at its root by keeping input out of the query parser entirely.