SQL Injection has been a known vulnerability for decades, yet it remains one of the most damaging and persistent software weaknesses in production systems. Its longevity is not due to a lack of awareness. It persists because developers continue to make incorrect assumptions about ORMs, stored procedures, “internal-only” inputs, and ad hoc query construction.
CWE-89 occurs when untrusted input is incorporated into a SQL query in a way that alters the intended command structure.
In practical terms:
The attacker turns your application’s database query into their own.
This article examines how SQL injection works, why teams still introduce it, modern exploitation techniques, framework-specific mitigations, and secure coding patterns.
What Is SQL Injection?
SQL Injection happens when application input is concatenated into SQL statements without proper parameterization.
Unsafe example:
query = "SELECT * FROM users WHERE username = '" + username + "'"
If an attacker supplies:
' OR 1=1 --
The final query becomes:
SELECT * FROM users WHERE username = '' OR 1=1 --'
The attacker has changed the logic of the query.
How SQL Injection Actually Works
The root problem is that SQL interprets attacker input as code instead of data.
Attack Flow
- Application receives user-controlled input
- Input is concatenated into SQL query
- Database parser treats input as SQL syntax
- Query logic changes
- Unauthorized data/actions occur
Visual: SQL Injection Data Flow
Why Developers Still Get SQL Injection Wrong
ORM False Confidence
Developers assume using an ORM means SQL injection is impossible.
Unsafe ORM usage still exists:
sequelize.query("SELECT * FROM users WHERE id = " + userId)
The ORM does not protect raw query construction.
Stored Procedure Misconceptions
Stored procedures are not automatically safe.
Unsafe dynamic SQL inside procedures remains vulnerable:
SET @sql = 'SELECT * FROM users WHERE name = ''' + @name + ''''
EXEC(@sql)
“Internal Inputs Are Trusted”
Attackers often exploit:
- Admin panels
- Internal APIs
- Background jobs
- Service-to-service parameters
Internal does not mean trusted.
Dynamic Query Requirements
Sorting, filtering, search, and reporting features pressure teams into dynamic SQL.
Developers often parameterize values but forget identifiers:
ORDER BY ` + userSortField
This can still be exploitable.
Modern Exploitation Techniques
Attackers rarely stop at simple authentication bypass.
Authentication Bypass
Classic payload:
' OR 1=1 --
UNION-Based Extraction
Append arbitrary results:
' UNION SELECT username,password FROM users --
Blind SQL Injection
Infer data through timing/behavior differences:
' AND IF(SUBSTRING(password,1,1)='a',SLEEP(5),0) --
Stacked Queries
Execute multiple statements where supported:
'; DROP TABLE users; --
OS-Level Code Execution
Some DBMSs support dangerous primitives:
- MSSQL
xp_cmdshell - PostgreSQL
COPY PROGRAM - MySQL file operations / UDF abuse
Visual: SQL Injection Exploitation Chain
Framework-Specific Mitigations
Raw SQL (Any Language)
Always use parameterized queries:
cursor.execute(
"SELECT * FROM users WHERE username = %s",
(username,)
)
Entity Framework / LINQ
Safe:
var user = db.Users.FirstOrDefault(u => u.Name == username);
Dangerous:
db.Database.ExecuteSqlRaw("SELECT * FROM Users WHERE Name = '" + username + "'");
Hibernate / JPA
Safe:
query.setParameter("name", username);
Avoid string concatenation in HQL/JPQL.
Node / Sequelize / Prisma
Use ORM parameter binding APIs rather than raw concatenated queries.
Secure Coding Examples
Unsafe
$sql = "SELECT * FROM users WHERE email = '$email'";
Safe
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);
Unsafe Dynamic ORDER BY
query = "SELECT * FROM users ORDER BY " + sortField
Safe Allowlist Pattern
allowed = {"name", "created_at", "email"}
sort = sortField if sortField in allowed else "name"
query = f"SELECT * FROM users ORDER BY {sort}"
Identifiers cannot generally be parameterized—use allowlists.
Defense in Depth
Least Privilege Database Accounts
Application DB users should not have:
- DROP TABLE
- ALTER SCHEMA
- Admin privileges
- OS-level execution rights
Error Handling
Do not expose raw SQL/database errors to users.
Verbose errors aid exploitation.
WAF / RASP
Can reduce commodity attacks but should not replace secure code.
Query Logging / Detection
Alert on:
- UNION SELECT patterns
- Excessive failed queries
- Time-based probes
- Suspicious comment syntax
Final Thoughts
SQL Injection remains one of the most damaging vulnerabilities because it converts application input directly into database control.
It persists because:
- Developers trust frameworks too much
- Raw queries creep back into codebases
- Dynamic SQL is harder than teams admit
- Internal/trusted inputs are wrongly exempted from scrutiny
The principle remains unchanged:
If untrusted input can alter query structure, you have SQL Injection.
Parameterization is not optional. It is the baseline.

2 thoughts on “CWE-89: SQL Injection — Why It Still Breaks Modern Applications”