Parameter

SQL injection (SQLi)

Also known as

  • SQL injection attack

SQL injection (SQLi) is a vulnerability where user input is concatenated into a database query, letting an attacker change the query's logic to read rows they should not see, bypass login checks, modify or delete data, and sometimes run commands on the database server.

OWASP
A03:2021 Injection
Last reviewed

How does SQL injection work?

SQL injection happens when an application builds a query by pasting user input into SQL text. The database cannot tell which part of the string the developer wrote and which part the user sent, so input that contains SQL syntax becomes part of the command.

A search endpoint that builds its query with string concatenation:

// Vulnerable: userId and q are pasted into the SQL text
const rows = await db.query(
  `SELECT id, name FROM projects WHERE owner_id = ${userId} AND name LIKE '%${q}%'`
);

A single quote in q closes the string literal early. Anything after it is parsed as SQL. The simplest proof is a pair of requests whose only difference is a condition that is always true versus always false:

GET /api/projects?q=demo%27%20AND%20%271%27%3D%271 HTTP/1.1
Host: app.example
Cookie: session=user_a
HTTP/1.1 200 OK
Content-Type: application/json

{"results": [{"id": "project_1043", "name": "demo-project"}]}

The same request with '1'='2 returns an empty list. If the input were treated as data, both requests would search for a project literally named with those quotes and return nothing. A result that flips with the condition proves the input is being executed.

What does SQL injection look like in practice?

The variants differ in how the attacker sees the result, which drives how a tester confirms them.

  • In-band. The query's output or error comes back in the response. Error-based injection shows a database error message; UNION-based injection appends a second SELECT whose rows appear in the page. Fast to confirm, loud in logs.
  • Blind, boolean-based. The response never shows query output, but it changes when a condition is true or false: a result appears or not, a status code differs, a page length shifts. The attacker extracts data one yes-or-no question at a time.
  • Blind, time-based. Nothing in the response changes, so the tester makes the database pause (pg_sleep(5) on PostgreSQL, SLEEP(5) on MySQL, WAITFOR DELAY on SQL Server) and measures the response time.
  • Out-of-band. The database is made to send a DNS or HTTP request to a host the tester controls. Rare, depends on database features and egress rules.
  • Second-order. The payload is stored safely on the way in, then used unsafely later. A username saved as demo'-- through a parameterized signup form breaks a nightly report job or an admin screen that builds its own query from stored values. Scanners that test one request at a time miss this almost entirely.

Where it hides in ORM code

ORMs parameterize by default, so modern SQL injection lives in the escape hatches:

StackSafeInjectable
Prisma$queryRaw tagged template$queryRawUnsafe with string input, Prisma.raw(userInput)
Djangofilter(), raw(sql, params)raw(f-string), extra(where=...), cursor.execute with % formatting
Railswhere(name: v), where("name = ?", v)where("name = '#{v}'"), order(Arel.sql(params[:sort]))
Sequelizereplacements, bind parameterssequelize.query with template literals
Anybound valuesORDER BY, column names, table names from input

The last row matters most. Identifiers and keywords cannot be bound as parameters, so sort columns, sort directions, and dynamic table names are the most common injection points in codebases that otherwise use an ORM correctly.

The best-known recent case is CVE-2023-34362 in Progress MOVEit Transfer, an unauthenticated SQL injection in the web application. NVD records it as CWE-89 and it was added to the Known Exploited Vulnerabilities catalog on June 2, 2023. CISA's advisory AA23-158A reports that the CL0P group began exploiting it on May 27, 2023, and used it to install a web shell named LEMURLOOT and take data from the underlying databases.

How do you test for SQL injection?

The goal is to prove the database is parsing your input, with the smallest possible change to the query.

  1. Map every input that reaches a query. Query strings, JSON bodies, headers, cookies, GraphQL arguments, and especially sort, order, filter, and column-selection parameters. Include values that are stored and reused later.
  2. Break the syntax. Send a single quote, a double quote, and a backslash. A 500 error, a database error string, or a changed result set on one of them is a lead, not a finding.
  3. Confirm with a boolean pair. Send a true condition and a false condition that should produce identical results if the input is data. For numeric parameters use 1043 AND 1=1 versus 1043 AND 1=2. A consistent difference across repeated requests confirms injection.
  4. Fall back to timing. If responses never change, append a conditional delay for each likely database engine and compare timings over several runs to rule out network jitter.
  5. Check sort and identifier parameters separately. Try sort=name versus sort=(SELECT 1) or an invalid column; an error that names the column, or a change in order, shows the value reaches SQL.
  6. Trace second-order paths. Store a quote-bearing value in a profile, filename, or tag, then trigger every feature that reads it back: exports, reports, admin views, background jobs.
  7. Stop at proof. A confirmed boolean or timing difference is enough for a report. Automated tools such as sqlmap can confirm and fingerprint the engine; run them only within the agreed scope.

Dynamic testing finds the in-band and simple blind cases. The ORM escape hatches and second-order paths are usually found faster in secure code review by grepping for raw-query methods and string-built SQL.

How do you fix SQL injection?

Use parameterized queries everywhere a value enters SQL, and use an allowlist everywhere an identifier does. Parameterization sends the query text and the values to the database separately, so input can never change the query's structure.

Node with Prisma, including a sort column that cannot be bound:

import { Prisma } from "@prisma/client";

const SORTABLE = { name: "name", created: "created_at" } as const;

export async function searchProjects(ownerId: string, q: string, sort: string) {
  const column = SORTABLE[sort as keyof typeof SORTABLE] ?? "created_at";
  return prisma.$queryRaw`
    SELECT id, name FROM projects
    WHERE owner_id = ${ownerId} AND name ILIKE ${"%" + q + "%"}
    ORDER BY ${Prisma.raw(column)}
  `;
}

Prisma.raw is only safe here because column comes from a fixed map, never from the request.

Django, with the ORM first and a raw query second:

from django.db import connection

# ORM: parameterized automatically
Project.objects.filter(owner_id=owner_id, name__icontains=q)

# Raw SQL: pass values as params, never format them into the string
with connection.cursor() as cur:
    cur.execute(
        "SELECT id, name FROM projects WHERE owner_id = %s AND name ILIKE %s",
        [owner_id, f"%{q}%"],
    )

Go with database/sql:

rows, err := db.QueryContext(ctx,
    "SELECT id, name FROM projects WHERE owner_id = $1 AND name ILIKE $2",
    ownerID, "%"+q+"%")

Then limit the damage: connect with a database role that has only the permissions the application needs, with no rights to system tables or file and command functions.

What does not work:

  • Escaping quotes by hand. Escaping rules differ by database, character set, and context, and they do nothing for numeric parameters or identifiers, which have no quotes to escape.
  • A WAF. It matches known payload shapes. Encodings, comments, and engine-specific syntax get past signatures, and second-order payloads arrive through a request that looks harmless. A WAF buys time while the query is fixed.
  • Input blocklists. Rejecting words like SELECT or OR breaks legitimate input and misses equivalent syntax.
  • Stored procedures alone. A procedure that builds dynamic SQL from its arguments is injectable in the same way.

Static analysis rules that flag raw-query methods receiving non-literal strings catch most regressions before they ship.

SQL injection vs other injection

SQL injection is one member of the injection family grouped under A03:2021 in the OWASP Top 10. NoSQL injection abuses query operators in document databases (a JSON object such as {"$ne": null} where a string was expected), command injection reaches a shell, and cross-site scripting injects into HTML and JavaScript in the victim's browser. The root cause is the same in all of them: data and code sharing one channel.

Written by Parameter · Last reviewed

[ related terms ]

Related terms.

Cross-site scripting (XSS)

Cross-site scripting (XSS) is a vulnerability where an application places attacker-supplied input into a page without encoding it, so the victim's browser runs the attacker's JavaScript with the site's origin and can read the page, act as the logged-in user, or change what they see.

OWASP Top 10

The OWASP Top 10 is the Open Worldwide Application Security Project's ranked list of the ten most critical web application security risk categories, such as broken access control and injection.

Dynamic application security testing (DAST)

Dynamic application security testing (DAST) is automated testing of a running web application or API from the outside: a scanner crawls the app, sends modified requests to each input, and flags responses that show injection, cross-site scripting, misconfiguration or exposed data, without access to source code.

Secure code review

Secure code review is the examination of source code, usually a pull request diff, specifically to find security flaws such as missing authorization checks, injection, unsafe deserialization and leaked secrets, by a person, a static analysis tool, an AI reviewer, or a combination, before the change reaches production.

Static application security testing (SAST)

Static application security testing (SAST) is automated analysis of source code, bytecode or binaries, without running the application, that traces untrusted input to dangerous operations such as SQL queries, shell commands and HTML output, and reports the file and line where an injection or similar flaw could occur.