What Is SQL Injection?
SQL injection (SQLi) is a vulnerability that allows an attacker to interfere with the database queries that an application makes. By inserting malicious SQL code into input fields, URL parameters, or HTTP headers, an attacker can manipulate the database query to do something the developer never intended.
The vulnerability exists whenever an application constructs SQL queries by concatenating user input directly into the query string. The database engine cannot distinguish between the developer's intended SQL and the attacker's injected code -- it executes all of it.
Despite being well-understood for over 25 years and having straightforward prevention methods, SQLi remains in the OWASP Top 10 and is still found in production applications. In 2023, OWASP merged it into the broader "Injection" category, which ranked third overall. The root cause is always the same: building queries with string concatenation instead of parameterized queries.
The consequences of a successful SQL injection attack range from unauthorized data access to complete database destruction. An attacker can read sensitive data, modify or delete records, execute administrative operations, and in some cases even gain control of the underlying operating system.
How SQL Injection Works
To understand SQL injection, you need to see how a vulnerable application processes user input. Consider a simple login form.
The Vulnerable Code
# Vulnerable Python code (DO NOT use this pattern)
username = request.form['username']
password = request.form['password']
query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"
result = db.execute(query)
When a normal user enters alice and mypassword, the query
becomes:
SELECT * FROM users WHERE username = 'alice' AND password = 'mypassword'
This works correctly. But what if an attacker enters this as the username?
' OR '1'='1' --
The resulting query becomes:
SELECT * FROM users WHERE username = '' OR '1'='1' --' AND password = ''
The OR '1'='1' condition is always true, and the --
comments out the rest of the query (the password check). The database returns all
users, and the application logs the attacker in as the first user in the table --
often the administrator.
Every SQL injection vulnerability begins with the application treating user input
as SQL code rather than as data. The single quote character ' breaks
out of the string literal in the query, allowing the attacker to inject their own
SQL. This is why string concatenation must never be used to build SQL queries with
user input.
Beyond Authentication Bypass
The login bypass above is the classic introductory example, but SQL injection can do far more damage. An attacker who can inject SQL can:
- Extract entire databases -- Read every table, including credentials, personal data, and financial records
- Modify data -- Change prices, elevate user privileges, alter records
- Delete data -- Drop tables or truncate critical data with
DROP TABLEorDELETE FROM - Read system files -- On MySQL,
LOAD_FILE('/etc/passwd')reads files from the server filesystem - Write files -- On MySQL,
INTO OUTFILEcan write files to disk, potentially creating a web shell - Execute OS commands -- On SQL Server,
xp_cmdshellcan execute arbitrary operating system commands
Types of SQL Injection
SQL injection attacks are classified by how the attacker retrieves results from the database. The type determines the attacker's technique, not the severity -- all types can lead to full database compromise.
Classic (In-Band) SQL Injection
In classic SQLi, the results of the injected query are displayed directly on the page. This is the simplest type to exploit because the attacker can immediately see the data they are extracting.
# Vulnerable URL:
https://shop.example.com/product?id=5
# The server runs:
SELECT name, price, description FROM products WHERE id = 5
# Attacker modifies the URL:
https://shop.example.com/product?id=5 OR 1=1
# The server runs:
SELECT name, price, description FROM products WHERE id = 5 OR 1=1
# Returns ALL products instead of just one
Union-Based SQL Injection
Union-based SQLi uses the UNION SELECT statement to append additional
queries to the original. This allows the attacker to extract data from entirely
different tables by combining results into the same output.
# Original query returns 3 columns (name, price, description)
# Attacker injects UNION SELECT to pull data from the users table:
https://shop.example.com/product?id=5 UNION SELECT username, password, email FROM users --
# The page now displays user credentials alongside product data
# The attacker must match the number of columns in the UNION
The key challenge in union-based attacks is determining the number and data types of
columns in the original query. Attackers typically do this by injecting
ORDER BY with increasing column numbers until they get an error, or by
using UNION SELECT NULL, NULL, NULL... until the column count matches.
Blind SQL Injection
Blind SQLi occurs when the application does not display query results or error messages on the page. The attacker cannot directly see the data, so they infer information by observing the application's behavior.
Boolean-Based Blind
The attacker asks true/false questions and observes whether the page changes. For example, a page that displays "Product found" or "Product not found" can be used to extract data one character at a time.
# Is the first character of the admin password 'a'?
https://shop.example.com/product?id=5 AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin') = 'a'
# If the page shows "Product found" → first character is 'a'
# If the page shows "Product not found" → first character is NOT 'a'
# Repeat for each character position and value
Time-Based Blind
When even the boolean response is not visible, attackers use time delays. If the injected condition is true, the database waits for a specified duration before responding.
# MySQL time-based blind injection:
https://shop.example.com/product?id=5 AND IF((SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin')='a', SLEEP(5), 0)
# If the response takes 5 seconds → first character is 'a'
# If the response is immediate → first character is NOT 'a'
Extracting data one character at a time is tedious for a human, but automated tools like sqlmap can extract entire databases through blind injection in minutes to hours, depending on the database size and response times.
Real-World SQL Injection Incidents
SQL injection has been behind some of the largest data breaches in history. These incidents demonstrate why prevention is critical.
- Heartland Payment Systems (2008) -- SQL injection was the initial entry point in a breach that exposed 130 million credit card numbers. The attackers used SQLi to install packet-sniffing malware on payment processing servers. It was the largest credit card breach at the time.
- Sony Pictures (2011) -- Attackers used a simple SQL injection to compromise databases containing personal information of over 77 million PlayStation Network accounts, including names, addresses, and credit card data. The breach cost Sony an estimated $171 million.
- TalkTalk (2015) -- A SQL injection attack on the UK telecom provider exposed personal data of 157,000 customers, including bank account details. TalkTalk was fined 400,000 pounds. The attack was carried out by teenagers using basic SQLi techniques.
- Equifax (2017) -- While the initial vulnerability was Apache Struts (not SQLi), the attackers used SQL injection techniques post-compromise to extract data from multiple databases, ultimately accessing records of 147 million people.
Many of these breaches did not require advanced skills or custom tools. Basic SQL injection attacks can be performed with nothing more than a web browser. The TalkTalk breach was carried out by a 15-year-old using freely available tools. Prevention is straightforward -- there is no excuse for shipping vulnerable code.
Prevention: Parameterized Queries
Parameterized queries (also called prepared statements) are the definitive solution to SQL injection. They work by separating the SQL code from the data, ensuring that user input is always treated as a value and never as executable SQL.
How They Work
Instead of concatenating user input into the query string, you define the query with placeholder parameters. The database driver sends the query structure and the data separately. The database engine compiles the query first, then binds the parameters as data values. It is structurally impossible for the data to alter the query logic.
Examples in Multiple Languages
# Python (with psycopg2 for PostgreSQL)
cursor.execute(
"SELECT * FROM users WHERE username = %s AND password = %s",
(username, password)
)
# Python (with SQLite3)
cursor.execute(
"SELECT * FROM users WHERE username = ? AND password = ?",
(username, password)
)
# PHP (with PDO)
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->execute(['username' => $username, 'password' => $password]);
# Java (with JDBC)
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE username = ? AND password = ?"
);
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
# Node.js (with mysql2)
const [rows] = await connection.execute(
'SELECT * FROM users WHERE username = ? AND password = ?',
[username, password]
);
When using parameterized queries correctly, the database engine never interprets
user input as SQL code. Even if an attacker enters ' OR '1'='1' --,
the database treats the entire string as a literal username value, not as SQL
syntax. The query returns no results because no user has that exact username.
Prevention: ORM Usage
Object-Relational Mappers (ORMs) provide an additional layer of abstraction over raw SQL. When used correctly, they automatically generate parameterized queries, making SQL injection nearly impossible by default.
ORM Examples
# Django ORM (Python) -- automatically parameterized
user = User.objects.filter(username=username, password=password).first()
# SQLAlchemy (Python) -- automatically parameterized
user = session.query(User).filter_by(username=username, password=password).first()
# Eloquent ORM (PHP/Laravel) -- automatically parameterized
$user = User::where('username', $username)->where('password', $password)->first();
# ActiveRecord (Ruby on Rails) -- automatically parameterized
user = User.find_by(username: username, password: password)
ORM Pitfalls
ORMs are safe by default, but most provide escape hatches for raw SQL. These escape hatches reintroduce the risk of SQL injection if used carelessly.
# DANGEROUS: Raw SQL in Django ORM
User.objects.raw("SELECT * FROM users WHERE username = '" + username + "'")
# SAFE: Raw SQL with parameters in Django ORM
User.objects.raw("SELECT * FROM users WHERE username = %s", [username])
# DANGEROUS: Raw SQL in SQLAlchemy
session.execute(text("SELECT * FROM users WHERE username = '" + username + "'"))
# SAFE: Raw SQL with parameters in SQLAlchemy
session.execute(text("SELECT * FROM users WHERE username = :name"), {"name": username})
Whenever you drop down to raw SQL within an ORM, you must use parameterized queries. The ORM's protection only applies to its query builder methods. Raw queries are your responsibility. Code review should flag any raw SQL that uses string concatenation or format strings with user input.
WAF Limitations and Defense in Depth
Web Application Firewalls (WAFs) are sometimes used to block SQL injection attempts by inspecting incoming HTTP requests for malicious patterns. While WAFs provide useful visibility and can catch basic attacks, they should never be your primary or only defense.
Why WAFs Are Not Enough
- Bypass techniques -- Attackers use encoding tricks (double URL encoding, Unicode substitution, comment injection) to evade WAF pattern matching. For every WAF rule, there are documented bypasses.
- False positives -- Aggressive WAF rules block legitimate requests. A user searching for "O'Brien" might trigger a SQL injection rule. This creates pressure to weaken the rules.
- No structural protection -- WAFs pattern-match on text. They cannot understand the SQL query structure or verify that parameterized queries are being used. They treat symptoms, not the disease.
- Maintenance burden -- WAF rules need constant updates as new bypass techniques are discovered. A set-and-forget WAF provides diminishing protection over time.
Defense in Depth Strategy
The correct approach is layered defense. No single measure is sufficient on its own.
# Example: Least-privilege MySQL user for a product catalog
CREATE USER 'catalog_reader'@'localhost' IDENTIFIED BY 'strong_random_password';
GRANT SELECT ON shop.products TO 'catalog_reader'@'localhost';
GRANT SELECT ON shop.categories TO 'catalog_reader'@'localhost';
-- This user CANNOT modify or delete data, even if SQLi is exploited
-- This user CANNOT access other databases or tables
Testing for SQL Injection
Identifying SQL injection vulnerabilities requires systematic testing of every input vector in the application. Here is a structured approach.
Manual Testing Steps
') into each input. If the application returns a database error
message, it is very likely vulnerable. Even a generic 500 error is suspicious.
id=5 AND 1=1 (should return normal results) versus
id=5 AND 1=2 (should return no results or different behavior).
If both behave differently, the input is being interpreted as SQL.
id=5 AND SLEEP(5) (MySQL) or
id=5; WAITFOR DELAY '0:0:5' (SQL Server). If the response is
delayed by 5 seconds, the application is vulnerable.
Automated Tools
- sqlmap -- The industry-standard open-source SQLi exploitation tool. It automates detection, exploitation, and data extraction across MySQL, PostgreSQL, SQL Server, Oracle, and SQLite. Use with explicit permission only.
- Burp Suite Scanner -- Professional web scanner with comprehensive SQLi detection. Identifies in-band, blind, and time-based vulnerabilities.
- OWASP ZAP -- Free open-source scanner with active SQL injection detection modules.
# sqlmap basic usage (only on systems you have permission to test)
sqlmap -u "https://example.com/product?id=5" --batch --level=3 --risk=2
# Options:
# -u Target URL with injectable parameter
# --batch Non-interactive mode (accepts defaults)
# --level=3 Thoroughness level (1-5)
# --risk=2 Risk of test payloads (1-3, higher = more aggressive)
# --dbs Enumerate databases after confirming injection
Only test for SQL injection on applications you own or have written authorization to test. Use practice environments like DVWA, WebGoat, HackTheBox, or PortSwigger Web Security Academy. Unauthorized testing is a criminal offense in most jurisdictions, regardless of whether you cause damage.
Summary
SQL injection is one of the oldest and most damaging web application vulnerabilities. Here is what you learned:
- SQL injection occurs when user input is concatenated directly into SQL queries, allowing attackers to manipulate the query logic
- Classic SQLi returns results directly on the page; Union-based SQLi combines data from multiple tables; Blind SQLi extracts data through boolean or time-based inference
- Real-world SQLi attacks have compromised hundreds of millions of records and caused billions of dollars in damages
- Parameterized queries are the definitive prevention -- they structurally separate SQL code from user data, making injection impossible
- ORMs use parameterized queries by default, but raw SQL escape hatches must still be parameterized
- WAFs are a useful monitoring layer but cannot replace secure coding practices due to bypass techniques and false positives
- Defense in depth combines parameterized queries, input validation, least-privilege database accounts, proper error handling, and WAF monitoring
You now understand how SQL injection works and how to prevent it at every layer. Combined with your knowledge of XSS and security headers, you have a solid foundation in web application security. Continue exploring the OWASP Top 10 to learn about additional vulnerability classes.