SQL injection was first documented in 1998. Twenty-five years later, it’s still in the OWASP Top 10 and responsible for major breaches. Why can’t we kill this bug?
The Anatomy of SQL Injection
At its core, SQL injection is about breaking context. When user input crosses the boundary from “data” to “code,” bad things happen.
// The classic vulnerable pattern
const query = `SELECT * FROM users WHERE email = '${email}'`;
// If email is: ' OR '1'='1
// Query becomes: SELECT * FROM users WHERE email = '' OR '1'='1'
// Result: Returns ALL users
Why It Persists
1. Legacy Code
Organizations have millions of lines of code written before secure coding was mainstream. Refactoring it all is expensive and risky.
2. Framework Misuse
Modern frameworks provide protection, but developers bypass it:
// Framework provides safe queries
User.where({ email: userEmail }); // Safe
// But developer goes raw for "performance"
db.query(`SELECT * FROM users WHERE email = '${userEmail}'`); // Vulnerable
3. Dynamic Query Requirements
Sometimes business logic seems to require dynamic queries:
// "We need dynamic column sorting"
const query = `SELECT * FROM products ORDER BY ${sortColumn} ${sortDirection}`;
The solution isn’t raw SQL—it’s whitelisting:
const allowedColumns = ['name', 'price', 'created_at'];
const allowedDirections = ['ASC', 'DESC'];
if (!allowedColumns.includes(sortColumn)) {
throw new Error('Invalid sort column');
}
if (!allowedDirections.includes(sortDirection)) {
throw new Error('Invalid sort direction');
}
Types of SQL Injection
In-Band SQLi
The classic: results appear directly in the response.
Blind SQLi
No direct output, but you can infer information:
- Boolean-based: Different responses for true/false
- Time-based: Delays indicate successful injection
-- Time-based blind injection
SELECT * FROM users WHERE id = 1 AND SLEEP(5)
-- If page takes 5 seconds, injection worked
Out-of-Band SQLi
Data exfiltrated through secondary channels (DNS, HTTP).
Complete Prevention
1. Parameterized Queries (Always)
// Node.js with prepared statements
const result = await db.query(
'SELECT * FROM users WHERE email = $1 AND status = $2',
[email, status]
);
2. ORM Usage (Correctly)
// Sequelize - safe by default
const user = await User.findOne({
where: { email: userEmail }
});
// Still vulnerable if using raw:
await sequelize.query(`SELECT * FROM users WHERE email = '${email}'`);
3. Stored Procedures (With Caution)
Stored procedures can help, but they’re not immune:
-- STILL VULNERABLE
CREATE PROCEDURE GetUser(IN userEmail VARCHAR(255))
BEGIN
SET @sql = CONCAT('SELECT * FROM users WHERE email = "', userEmail, '"');
PREPARE stmt FROM @sql;
EXECUTE stmt;
END
4. Input Validation (Defense in Depth)
Validation shouldn’t be your only defense, but it helps:
const emailSchema = z.string().email().max(254);
const validatedEmail = emailSchema.parse(userInput);
Testing for SQL Injection
Manual testing patterns:
- Single quote:
' - Double quote:
" - SQL comments:
--,#,/* */ - Boolean tests:
' OR '1'='1,' AND '1'='2 - Time delays:
'; WAITFOR DELAY '0:0:5'--
Automated tools like Hacker Bot test these patterns and hundreds more against every input vector in your application.
Conclusion
SQL injection persists because security is hard and legacy code is everywhere. The fix is simple—use parameterized queries everywhere—but the discipline to maintain that standard is the real challenge.
Hacker Bot automatically tests for SQL injection across your entire attack surface. Find out what you’re missing.
