SQL injection. It's been on the OWASP Top 10 since... forever. We've known how to prevent it for over 20 years. Yet here we are in 2024, and SQL injection is still causing massive breaches.
What's going on?
The Persistent Problem
SQL injection vulnerabilities occur when an application includes untrusted data in SQL queries without proper sanitization. Attackers can inject malicious SQL code, potentially:
- Extracting entire databases
- Modifying or deleting data
- Bypassing authentication
- Executing operating system commands (in some configurations)
We've had the solution forever: use parameterized queries (prepared statements). So why does this keep happening?
Why SQL Injection Persists
1. Legacy Code
Organizations have millions of lines of code written before secure coding was standard practice. That old PHP app from 2008? Probably vulnerable.
Rewriting legacy systems is expensive and risky. So they keep running, vulnerable SQL injection and all.
2. Framework Misuse
Modern frameworks make it easy to use parameterized queries. They also make it easy to bypass those protections when you need "dynamic" queries.
Example (dangerous):
const query = `SELECT * FROM users WHERE username = '${req.body.username}'`;
db.query(query);
The developer knew about SQL injection but thought "this input is validated on the frontend" or "I need dynamic query building here."
3. Complex Queries
Sometimes you need to dynamically build complex queries - filtering, sorting, pagination. Developers often resort to string concatenation because it's easier than properly parameterizing everything.
4. ORMs Aren't Perfect
Object-Relational Mappers (ORMs) handle most SQL safely, but they usually have "raw query" escape hatches for complex operations. Developers use those and reintroduce SQL injection.
5. New Developers
Every year, new developers enter the field. Not all of them learn secure coding practices from day one. They make the same mistakes we've been making for decades.
6. Time Pressure
"Ship it now, we'll fix security later." Spoiler: later rarely comes.
Modern SQL Injection Techniques
Attackers have evolved their techniques:
Time-Based Blind SQL Injection
Even when you don't see query results, attackers can exfiltrate data by measuring response times:
IF (SELECT COUNT(*) FROM users WHERE username='admin') > 0
WAITFOR DELAY '00:00:05'
If the response takes 5 seconds, the condition was true.
Second-Order SQL Injection
The malicious payload gets stored in the database, then executed later in a different part of the application:
- Attacker registers with username:
admin'-- - Application safely stores this in the database
- Later, a different function retrieves this username and uses it in an unsafe query
- SQL injection executes in step 3, not step 1
NoSQL Injection
Plot twist: NoSQL databases can be vulnerable too!
MongoDB example:
db.users.find({ username: req.body.username });
If req.body.username is { $ne: null }, it matches ALL users.
ORM Injection
Even ORMs can be exploited if you're not careful:
User.where("name = '#{params[:name]}'")
That's Ruby on Rails with raw SQL conditions - vulnerable to injection.
Real-World Impact
SQL injection isn't theoretical. Major breaches include:
- TalkTalk (2015): 157,000 customer records stolen via SQL injection
- Fortnite (2019): Account takeover vulnerability using SQL injection
- Freepik (2020): 8.3 million user records exposed
- Thousands of unnamed SMBs: Many breaches never make headlines
The average cost of a data breach in 2024? Over $4 million.
Defense Strategies
The good news: SQL injection is very preventable.
1. Parameterized Queries (Always)
The gold standard:
cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
The database engine treats parameters as data, never as executable code.
2. Stored Procedures (Done Right)
Stored procedures can help, but only if they use parameterization internally. Don't just move the vulnerable code into a stored procedure.
3. Input Validation
Validate input types and formats. If you're expecting a number, ensure it's actually a number.
BUT: Don't rely on input validation alone. Always use parameterized queries.
4. Principle of Least Privilege
Database accounts should have minimal necessary permissions. Your web app probably doesn't need DROP TABLE rights.
5. Web Application Firewalls (WAF)
WAFs can detect and block SQL injection attempts. They're not a substitute for proper coding, but they add defense in depth.
6. Static Analysis
Tools like SonarQube, Snyk Code, and Checkmarx can automatically detect SQL injection vulnerabilities in code.
7. Runtime Protection
Runtime application self-protection (RASP) tools monitor application behavior and can block SQL injection attempts.
Testing for SQL Injection
How do you know if you're vulnerable?
Manual Testing
Basic tests:
- Add a single quote (
') to input fields - do you get database errors? - Try
' OR 1=1--- does behavior change? - Submit
' AND SLEEP(5)--- does the response delay?
Automated Scanning
Tools like:
- SQLmap (free, powerful)
- Burp Suite (commercial)
- OWASP ZAP (free)
Code Review
Review all database queries in your codebase. Look for:
- String concatenation in queries
- Raw SQL in ORMs
- User input directly in queries
The Framework-Specific Guide
PHP/MySQL
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$userId]);
Python/PostgreSQL
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
Node.js/MySQL
connection.query("SELECT * FROM users WHERE id = ?", [userId], callback);
Java/JDBC
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setInt(1, userId);
C#/.NET
var command = new SqlCommand("SELECT * FROM users WHERE id = @id", connection);
command.Parameters.AddWithValue("@id", userId);
Common Misconceptions
"I'm using an ORM, so I'm safe" ORMs help, but raw queries and unsafe method calls can still introduce vulnerabilities.
"I validate input on the frontend" Client-side validation can be bypassed. Always validate and sanitize server-side.
"I escape special characters" Manual escaping is error-prone. Use parameterized queries instead.
"My database user has limited permissions" Good! But attackers can still steal whatever data that user can access.
"We have a WAF" Defense in depth is good, but WAFs can be bypassed. Fix the code.
Making SQL Injection History
To actually eliminate SQL injection, we need:
1. Education: Teach secure coding from day one 2. Tooling: Make the secure way the easy way 3. Enforcement: Block unsafe code in CI/CD pipelines 4. Culture: Security is everyone's responsibility 5. Legacy Remediation: Dedicated time to fix old code
The Bottom Line
SQL injection in 2024 is inexcusable. We know how to prevent it. We have the tools. We have the frameworks.
What we need is discipline: always use parameterized queries, no exceptions. Review code for database interactions. Test regularly.
It's time to make SQL injection a thing of the past. One codebase at a time.