CWE-89: SQL Injection — Why It Still Breaks Modern Applications

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

  1. Application receives user-controlled input
  2. Input is concatenated into SQL query
  3. Database parser treats input as SQL syntax
  4. Query logic changes
  5. Unauthorized data/actions occur

Visual: SQL Injection Data Flow

1. User Input ‘ OR 1=1 — 2. Query Concatenation Unsafe String Building 3. Database Parses as SQL Code 4. Result Data Exposure

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

SQL Injection Auth Bypass Data Extraction Privilege Escalation Potential RCE / DB Takeover

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.