CWE-89OWASP A03:2021 Injection

SQL injection (SQLi)

User input is built into a database query as code instead of being passed as data, letting an attacker read, change or delete data and sometimes take over the database server.

Also known asBlind SQL injection · Error-based SQL injection · Second-order SQL injection
Typical severityHigh to Critical
ClassificationCWE-89
Last updatedOct 14, 2026

What it is#

SQL injection occurs when an application builds a SQL statement by joining strings, and part of that string comes from an attacker. Characters like ', -- and ; change the structure of the query, so the database executes logic the developer never wrote.

Common variants:

  • In-band (error-based or UNION-based): results or database errors come back in the response.
  • Blind (boolean or time-based): nothing is shown directly, but the attacker infers data from response differences or delays.
  • Second-order: input is stored safely, then later concatenated into a different query, often in a background job or report.

Why it matters#

  • Read any data the application's database account can reach: customer records, password hashes, API tokens.
  • Bypass authentication by making a login query always return true.
  • Modify or delete data, including audit logs.
  • On some platforms and configurations, read files, call stored procedures or run operating system commands, turning a web bug into server compromise.
Typical severity

High to critical. Unauthenticated SQL injection on an internet-facing application is commonly CVSS 3.1 9.8.

Examples#

python
# Vulnerable: string formatting builds the query
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")
java
// Vulnerable: concatenation in JDBC
String q = "SELECT * FROM orders WHERE id = " + request.getParameter("id");
ResultSet rs = stmt.executeQuery(q);

A tester confirming the issue typically sends a single quote and looks for a database error, or compares responses to id=5 AND 1=1 versus id=5 AND 1=2.

How to fix it#

1. Use parameterized queries everywhere. The query structure is sent separately from the values, so input can never become code.

python
# Fixed: parameter binding (psycopg / DB-API)
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
java
// Fixed: PreparedStatement
PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE id = ?");
ps.setLong(1, Long.parseLong(request.getParameter("id")));
javascript
// Fixed: node-postgres
await pool.query('SELECT * FROM orders WHERE id = $1', [req.params.id]);

2. Use ORM features safely. ORMs parameterize by default, but raw-query helpers (raw(), $queryRawUnsafe, extra(), string-built where clauses) bring the risk back.

3. Allow-list identifiers. Table names, column names and sort directions can't be bound as parameters. Map user choices to fixed values:

python
SORTS = {"name": "name", "created": "created_at"}
order_by = SORTS.get(request.args.get("sort"), "created_at")

4. Least privilege. Give the application's database account only the permissions it needs; never use a superuser or sa account. Disable features like xp_cmdshell.

5. Don't leak errors. Return generic errors to users and log details server-side.

How to test for it#

  • Inventory every input that reaches a query, including headers, cookies, JSON bodies, sort/filter parameters and stored values reused later.
  • Probe with syntax-breaking characters and boolean or time-based conditions; compare responses.
  • Use tools such as sqlmap carefully and only within an authorized scope, with non-destructive options.
  • Review code for string-built queries and unsafe ORM calls; add a static analysis rule to block them.
  • After fixing, retest each parameter and add regression tests.

References#