🌐 This article is also available in: Deutsch

SQL Injection: How It Works, Examples & Prevention

SQL injection basics: attack types and prevention with parameterized queries

SQL injection (SQLi) is one of the oldest and most persistent vulnerabilities affecting web applications. In such an attack, an attacker slips their own SQL code into a database query through ordinary input fields, URL parameters or HTTP headers. When there is no clean separation between program code and user input, the database interprets that input as a command instead of as a plain data value. The consequences range from bypassing authentication to reading entire databases and, in the worst case, taking over the server completely.

This article covers the fundamentals thoroughly and precisely: what SQL injection actually is on a technical level, how an attack unfolds, which attack types exist and, as its centerpiece, how to protect yourself effectively with parameterized queries, including runnable code examples across several programming languages. For the deeper exploitation and filter-bypass techniques, you will find a pointer at the end to our advanced SQL injection article.

If you want a broader picture of the web application threat landscape, our overview of the OWASP Top 10 provides useful context.

What Is a SQL Injection?

SQL injection is the classic vulnerability from the injection family. The formal definition comes from the MITRE classification CWE-89 (Improper Neutralization of Special Elements used in an SQL Command). In essence: an application constructs all or part of an SQL command from externally influenced input, but it does not neutralize, or incorrectly neutralizes, the special SQL characters that can modify the intended command.

The root of the problem is the missing separation of code and data. An SQL query consists of a fixed command skeleton (such as SELECT ... FROM ... WHERE ...) and variable values (such as an entered username). If those values are simply copied into the command as strings (string concatenation), an attacker can smuggle in characters that break out of the data region and change the structure of the command itself. That, precisely, is a SQL injection.

According to MITRE, the impact is wide-ranging:

  • Confidentiality: Since SQL databases usually hold sensitive data, loss of confidentiality is a frequent problem (leaking customer records, password hashes, payment information).
  • Integrity: Attackers can modify or delete data.
  • Authentication: Attackers can log in as another user without knowing that user's password.
  • Code execution: In the worst case, for example via dangerous database features such as xp_cmdshell on Microsoft SQL Server, a SQL injection can even escalate to operating-system command execution.

Mapping to the OWASP Top 10 and CWE

SQL injection is not an obscure edge case; it has been a fixture of the most important security standards for years. The mapping should be edition-accurate, because the OWASP Top 10 changed their numbering between editions:

  • OWASP Top 10:2021: SQL injection falls under the category A03:2021 Injection. CWE-89 is explicitly listed as a mapped weakness on the official category page.
  • OWASP Top 10:2025: In the 2025 edition, the Injection category moved to A05:2025 (two places lower). CWE-89 remains explicitly listed among the mapped CWEs here as well.

Terminological precision matters here: the OWASP "Injection" category is broader than SQL injection alone. It also covers NoSQL, OS command, ORM, LDAP and Expression Language injection, among others. SQL injection (CWE-89) is a specific, prominent member of this family, not a synonym for the whole category.

The shift in ranking can be described soberly: injection weaknesses have consistently ranked among the top-5 risks since the first modern OWASP Top 10 lists. Their relative rank has slowly declined only because other categories grew faster in importance. Yet for the Injection category alone, OWASP reports over 62,000 CVEs in the 2025 edition, of which, by its own account, more than 14,000 CVEs relate specifically to SQL injection. The takeaway: the vulnerability is old, but far from resolved.

How Does a SQL Injection Work?

The principle is clearest with a login form. An insecure application might insert the entered values directly into the query:

SELECT * FROM Users WHERE Username = 'EnteredUser' AND Password = 'EnteredPassword';

If an attacker now enters the following string as the username:

' OR '1'='1'; --

then the assembled query changes to:

SELECT * FROM Users WHERE Username = '' OR '1'='1'; --' AND Password = 'EnteredPassword';

Two things happen here: the condition '1'='1' is always true, and the double hyphen -- starts a comment in SQL, so the entire rest of the query (the password check) is ignored. The database returns a valid user record (typically the first row) without any valid password. Authentication is bypassed.

A second, equally classic pattern comes from the PortSwigger training material. A product page filters goods by category:

https://insecure-website.com/products?category=Gifts'-- 

On the server this becomes:

SELECT * FROM products WHERE category = 'Gifts'-- ' AND released = 1

The commented-out part AND released = 1 disappears, so even unreleased products are shown. Note that in MySQL the -- comment must be followed by a whitespace character to take effect, which is why the payload ends with a trailing space. Both examples show the same underlying pattern: a single quote breaks out of the data region, and the rest is up to the attacker's creativity.

The Types of SQL Injection at a Glance

SQL injection is not a single, uniform attack but a whole family of techniques. They differ mainly in how the attacker receives the database's answer. The following taxonomy follows the official definitions from OWASP and PortSwigger.

In-Band SQL Injection

In the in-band variant, the attacker uses the same communication channel for both the attack and the result; the data comes back directly in the application's response. This is the simplest and most common form.

Error-based: The attacker deliberately provokes database errors whose verbose messages reveal structure, table names or column names. Leaky error messages in the frontend are the source of information here.

UNION-based: Using the UNION keyword, the attacker appends the results of a second, attacker-controlled query to the original result. This requires the injected query to return the same number of columns with compatible data types. A canonical example:

' UNION SELECT username, password FROM users--

This injects credentials from a completely different table into the visible result list.

Blind SQL Injection

In the blind variant, the application returns neither the query results nor verbose error messages directly. The attacker must instead ask the database true-or-false questions and infer the answer from the application's behavior.

Boolean-based (content-based): The attacker appends a condition and observes whether the page content changes. The classic contrast:

and 1=1
and 1=2

1=1 is always true and returns the normal page, 1=2 is always false and returns different output. By systematically querying individual characters (for example with SUBSTRING), an attacker can extract a password hash character by character.

Time-based: When even the content reveals no difference, the attacker uses database-specific delay functions. A measurably longer response time signals that the condition was true. Examples:

' waitfor delay '00:00:10'--

(Microsoft SQL Server; MySQL uses SLEEP(), PostgreSQL uses pg_sleep().) Because each extracted character needs its own request, blind SQL injection is very time-consuming by hand and is usually automated with tools such as SQLMap.

Out-of-Band SQL Injection

When neither content nor timing returns an answer, the attacker can make the database initiate contact over a second channel, for example a DNS or HTTP request to a server they control. This out-of-band technique is especially relevant for strictly asynchronous request processing.

Second-Order (Stored) SQL Injection

The most insidious variant: the malicious value is first stored completely harmlessly and only later, at a very different point in the application, incorporated unsafely into a new query. PortSwigger defines it as follows: "Second-order SQL injection arises when user-supplied data is stored by the application and later incorporated into SQL queries in an unsafe way."

A typical pattern: an attacker registers a username containing SQL syntax. When it is saved, a parameterized query correctly stores the value as harmless text. Later, a reporting or admin feature reads that value back and inserts it via string concatenation into a new query, where it then acts as SQL. The key lesson: it is not enough to parameterize only the writing query. Every query that touches this data, at every point in the application, must be parameterized.

How these types are combined in practice and pushed past filters, including concrete bypass tricks, is covered in depth in our advanced SQL injection article.

Protection with Prepared Statements: the Most Effective Control

By far the most important defense against SQL injection is parameterized queries, also known as prepared statements. The OWASP Cheat Sheet lists them as "Primary Defense Option 1," ahead of every other measure. The principle: the developer first defines the complete SQL command skeleton with placeholders and only afterwards passes the user values separately. As a result, the database always distinguishes between code and data, no matter what input arrives.

Understanding the insecure counter-pattern is essential. Wherever input is built into a query via string concatenation, the gap appears. The following examples therefore show the vulnerable anti-pattern first, then the secure, parameterized variant, following the official documentation of each language verbatim.

PHP with PDO

<?php
// INSECURE: user input concatenated directly into the query
$sql = "SELECT * FROM users WHERE email = '" . $_GET['email'] . "'";
$stmt = $dbh->query($sql);

Secure with named placeholders (:calories, :colour), whose values are passed as an array to execute():

<?php
/* Prepared statement, values passed as an array */
$sql = 'SELECT name, colour, calories
    FROM fruit
    WHERE calories < :calories AND colour = :colour';
$sth = $dbh->prepare($sql, [PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY]);
$sth->execute(['calories' => 150, 'colour' => 'red']);
$red = $sth->fetchAll();
?>

As a supplementary note, secondary sources recommend setting PDO::ATTR_EMULATE_PREPARES to false so that the separation of structure and data happens at the protocol level rather than being emulated client-side. This is a refinement worth double-checking against the PDO manual if needed; injection safety is already provided by the parameterized passing itself.

PHP with mysqli

Instead of the frequently recommended escaping function mysqli_real_escape_string() (which only escapes and is not a true prepared statement), you should use prepare() and bind_param(). The type string 'sssd' declares each parameter (s = string, d = double), adding an extra layer of type safety:

<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli('localhost', 'my_user', 'my_password', 'world');

$stmt = $mysqli->prepare("INSERT INTO CountryLanguage VALUES (?, ?, ?, ?)");
$stmt->bind_param('sssd', $code, $language, $official, $percent);

$code = 'DEU';
$language = 'Bavarian';
$official = "F";
$percent = 11.2;

$stmt->execute();

printf("%d row inserted.\n", $stmt->affected_rows);
?>

Java with JDBC PreparedStatement

The vulnerable pattern builds the query via concatenation:

// INSECURE
String query = "SELECT * FROM products WHERE category = '" + input + "'";

Secure with PreparedStatement and the placeholder ?, whose value is bound via setString():

String custname = request.getParameter("customerName");
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, custname);
ResultSet results = pstmt.executeQuery();

Python with the DB-API

The official Python documentation explicitly warns against string formatting ("Never do this -- insecure!"):

# INSECURE: never do this, per the official docs
symbol = input()
sql = "SELECT * FROM stocks WHERE symbol = '%s'" % symbol
cur.execute(sql)

The secure way is the DB-API's parameter substitution using the placeholder ? (with sqlite3) or %s with many other drivers such as psycopg2. Important: the %s placeholder here is not string formatting but genuine parameterization; the values are passed separately:

import sqlite3

con = sqlite3.connect(":memory:")
cur = con.execute("CREATE TABLE lang(name, first_appeared)")

# qmark style in a SELECT query:
params = (1972,)
cur.execute("SELECT * FROM lang WHERE first_appeared = ?", params)
print(cur.fetchall())
con.close()

Node.js with Parameterized Queries

The PostgreSQL driver pg uses numbered placeholders ($1, $2); the values are passed in a separate array, never interpolated into the query string:

const text = 'INSERT INTO users(name, email) VALUES($1, $2) RETURNING *'
const values = ['brianc', '[email protected]']

const res = await client.query(text, values)
console.log(res.rows[0])

The same principle applies across languages: PHP, Java, Python and Node.js all separate the fixed structure from the variable values. Parameterization is not a quirk of a single language but the universal underlying pattern.

The ORM Trap: Safe, but Not Automatically

An Object-Relational Mapper (ORM) such as Django or Hibernate parameterizes standard queries automatically and is therefore safe in the normal case. A widespread misconception, however, is that an ORM protects against SQL injection wholesale. That is only true as long as you stay inside the safe query API. As soon as raw queries or string concatenation enter the picture, the gap returns. MITRE even maintains a dedicated classification for this: CWE-564 (SQL Injection: Hibernate).

In Django, exactly four escape hatches bypass automatic parameterization: .extra(), .raw(), RawSQL and the direct database cursor. A vulnerable example:

# INSECURE: f-string interpolation inside .extra()
products = Product.objects.extra(
    where=[f"name LIKE '%{search_term}%' AND category = '{category}'"],
    order_by=[sort_field]
)

The secure solution stays inside the parameterized ORM API using Q() objects and filter():

queryset = Product.objects.all()
if validated_data['search_term']:
    queryset = queryset.filter(
        Q(name__icontains=validated_data['search_term']) |
        Q(description__icontains=validated_data['search_term'])
    )
if validated_data['category']:
    queryset = queryset.filter(category=validated_data['category'])
queryset = queryset.order_by(validated_data['sort_field'])

The clear message: an ORM is not an automatic safeguard once raw queries or string concatenation are involved. The same applies to Hibernate when HQL is built by hand instead of using named parameters.

Allowlist for Non-Parameterizable Elements

Placeholders only work for data values, never for structural elements such as table names, column names or the sort direction (ASC/DESC). These are part of the SQL structure, not the data, and cannot be bound as parameters. If such an element must come from user input, an allowlist is the only correct safeguard: you map allowed inputs to a fixed set of permitted values hard-coded in your source, and reject everything else.

String tableName;
switch (PARAM) {
  case "Value1": tableName = "fooTable"; break;
  case "Value2": tableName = "barTable"; break;
  default: throw new InputValidationException("unexpected value provided");
}

For the sort direction, a boolean mapping to the fixed literals suffices instead of an arbitrary string:

public String someMethod(boolean sortOrder) {
  String SQLquery = "some SQL ... order by Salary " + (sortOrder ? "ASC" : "DESC");
}

Never insert the raw user value directly as a table name or ORDER BY clause.

Defense in Depth: Multiple Layers Instead of One

Prepared statements are the primary defense, but real security comes from several interlocking layers. The OWASP Cheat Sheet and the CWE-89 recommendations complement parameterization with the following building blocks.

Allowlist validation as an additional layer: input should be validated server-side against a positive list of allowed characters and formats in all cases, even when parameterization is already in place. Validation and parameterization solve different problems; validated but string-built input can still be unsafe.

Least privilege for database users: the application account should hold only the minimum necessary permissions. OWASP states this with an explicit warning: never assign DBA or admin access to your application accounts. It recommends separate database users per application, read-only access where writes are unnecessary, views to restrict field-level access, and never running the database process as root or system. This limits the damage even if an injection succeeds.

Web Application Firewall (WAF) as an additional, not replacing, layer: a WAF can catch known attack patterns and buy time, for example for legacy systems that are not yet patched. But it is no substitute for secure code, because attackers continually develop techniques to bypass filters. A WAF complements prepared statements; it does not replace them.

Why Escaping and Blocklists Alone Are Not Enough

A common anti-pattern is trying to prevent SQL injection solely by escaping special characters or by blocklisting forbidden words. OWASP explicitly rates escaping all input as "STRONGLY DISCOURAGED": it is highly database-specific, error-prone and fragile compared to other defenses. Blocklists can be evaded through alternative spellings, comments or encodings. The mysqli_real_escape_string() approach shown in the old legacy article is exactly this kind of weaker, escaping-only technique and is no substitute for a genuine prepared statement. Always rely on parameterization as the primary control and treat escaping, at best, as a last resort.

Real-World Cases: From CVE to Multi-Million Losses

SQL injection is not an academic exercise. Two examples illustrate the range.

A recent CVE example: the entry CVE-2024-2333 (published on 9 March 2024, verified against the NVD database) affects the niche product CodeAstro Membership Management System 1.0. According to the NVD description, manipulation of the fullname argument in the file /add_members.php leads to a SQL injection, classified as CWE-89. Notably, the two scoring sources cited on the NVD page arrive at different severities (NIST 7.2 HIGH, VulDB 6.3 MEDIUM), depending on assumptions about required privileges. This shows that severity is not an objective single number. The example serves here only to illustrate that such vulnerabilities are still documented regularly today.

A well-known historical incident (with a source caveat): the 2015 TalkTalk data breach is considered a textbook case. According to predominantly secondary sources (Wikipedia, press reports, the ICO investigation), attackers exploited SQL injection vulnerabilities in three outdated legacy pages inherited from an acquisition to access 156,959 customer accounts. The UK's Information Commissioner's Office (ICO) subsequently imposed a fine that, according to these sources, amounted to 400,000 pounds. The exact fine figure is only supported by secondary sources (the original ICO page was not directly reachable during research), which is why we cite it explicitly with a caveat. The core lesson is undisputed: forgotten, unpatched legacy systems are an entry point, and a freely available tool like SQLMap was enough to find the flaw.

Distinction from Related Attacks

SQL injection vs. NoSQL injection: NoSQL databases such as MongoDB use no standardized query language but flexible, document-based structures. The underlying principle stays the same (the attacker breaks the intended query structure), but the payloads differ significantly, for example via operators such as $ne or $regex in JSON. An example of an authentication bypass:

{"username":{"$ne":"invalid"},"password":{"$ne":"invalid"}}

With NoSQL, an additional control is allowlisting the permitted operator keys (for example rejecting keys that begin with $).

SQL injection vs. command injection: both belong to the broad injection family but target different interpreters. SQL injection manipulates database queries; command injection injects commands into the operating system. How a SQL injection can escalate to the operating-system level via features such as xp_cmdshell is shown in our article on remote code execution.

Also related is the circumvention of access controls, which we cover under broken access control and IDOR.

Another close relative in the web space is cross-site scripting (XSS), which also stems from a lack of separation between code and data, though in the victim's browser rather than in the database.

Frequently Asked Questions

What is SQL injection?

SQL injection is a vulnerability in which an attacker slips their own SQL code into a database query through ordinary input fields. It is formally described by CWE-89. The root cause is always the same: the application fails to separate program code from user input cleanly, so the database interprets the input as a command instead of a plain data value.

How does a SQL injection attack work?

An application builds a query by concatenating user input, such as SELECT * FROM Users WHERE Username = '...'. If an attacker enters ' OR '1'='1'; -- as the username, the single quote breaks out of the data region, the condition '1'='1' becomes always true, and the -- comments out the password check. The database then returns a valid user record without any password, bypassing authentication.

What is an example of a SQL injection?

A classic example is UNION-based injection. By appending ' UNION SELECT username, password FROM users-- to a vulnerable parameter, an attacker attaches the results of their own query to the original one and pulls credentials from a completely different table into the visible output, provided the injected query returns a matching number of columns.

How do you prevent SQL injection?

The most effective control is parameterized queries (prepared statements), which OWASP lists as its Primary Defense. The command skeleton with placeholders is fixed, and user values are passed separately afterwards, so the database never confuses code and data. Complement this with allowlist validation for structural elements, the least-privilege principle for database accounts, and a WAF as an additional layer. Escaping or blocklists alone are not enough.

Does an ORM protect against SQL injection?

Only partially. An ORM such as Django or Hibernate parameterizes standard queries automatically and is safe in the normal case. That protection holds only as long as you stay inside the safe query API. As soon as raw queries (.raw(), .extra()) or string concatenation are involved, the vulnerability returns. MITRE tracks this under its own classification, CWE-564.

Conclusion

SQL injection remains one of the most consequential vulnerabilities for web applications and databases. Formally described by CWE-89 and mapped under OWASP A03:2021 (respectively A05:2025) Injection, it always arises from the same root cause: the missing separation of program code and user input. The attack types range from in-band (error-based, UNION-based) through blind (boolean- and time-based) to out-of-band and the especially insidious second-order variant.

The most effective protection is clear: parameterized queries (prepared statements) in every language and at every point in the application, complemented by allowlist validation for non-parameterizable structural elements, the least-privilege principle for database accounts, and a WAF as an additional layer. Plain escaping or blocklists are explicitly insufficient. And be careful with ORMs: they only protect as long as you stay inside their safe API and do not drop into raw queries.

If you want to understand the exploitation side in more depth, including concrete filter-bypass techniques and a practical case study, continue reading in our advanced SQL injection article.