Defense in Depth
Parameterisation removes the bug; these layers limit the blast radius when a parameterisation is missed. Least privilege, allowlists, monitoring, and what each is actually worth.
One Fix, Several Mitigations
There is exactly one fix for SQL injection: parameterised queries, everywhere, without exception. Everything else on this page is a mitigation — it reduces the damage when a query is missed, but it does not make the application safe on its own.
The distinction matters because mitigations are routinely oversold. A WAF, stored procedures, or input validation are each sometimes presented as "our SQL injection protection". None of them are. They are layers that buy time and limit blast radius, and they are valuable precisely because humans do miss the occasional query — but they are not substitutes for the fix.
The layers, roughly in order of value:
- Parameterise (the fix).
- Least privilege (limits what any injection can do).
- Allowlist identifiers (covers the one gap parameterisation cannot).
- Reduce the reachable attack surface.
- Monitor and rate-limit (detection).
- WAF (delay and signal).
Least Privilege — The Mitigation That Matters Most
If parameterisation is the fix, least privilege is the mitigation to prioritise, because it caps the severity of every injection that slips through.
An application account should hold only what the application uses:
- DML on its own schema only —
SELECT,INSERT,UPDATE,DELETEon the tables it touches. NotDROP, notCREATE, notALTER. - No administrative role — never
sysadmin/sa(MSSQL), superuser (PostgreSQL),DBA(Oracle), orSUPER/FILE(MySQL). - No file privileges — the difference between data disclosure and file read/write or RCE.
- Separate accounts for separate trust levels — a read-only reporting account, a read-write application account, a migration account used only at deploy time. A read-only account turns a catastrophic injection into a disclosure.
- Restricted
information_schemavisibility where the engine supports it, so schema enumeration returns less.
Walk back through the escalation and DBMS reference guides: nearly every high-impact technique requires a privilege the application does not need. Remove the privilege and the technique is inert even against a live injection.
Allowlist Identifiers
Parameterisation cannot bind a table name, column name, or sort direction — no placeholder syntax accepts an identifier. This is the one gap the fix leaves open, and it is where an otherwise-parameterised codebase gets injected.
Wherever an identifier must be dynamic, map user input to a fixed set of known-good values and reject anything else:
const SORTABLE = { name: 'name', price: 'price', date: 'created_at' }
const column = SORTABLE[req.query.sort] ?? 'id' // never the raw input
const dir = req.query.dir === 'desc' ? 'DESC' : 'ASC'
The value that reaches the SQL is one you wrote. See Injection Contexts for why this context is special and ORM Injection for how it recurs in every framework.
Reduce the Reachable Surface
Independently of any injection, remove the primitives an attacker would reach for. Each of these closes an entire technique:
- MSSQL:
xp_cmdshell,Ole Automation Procedures,clr enabledoff; remove unused linked servers; dropTRUSTWORTHY. - PostgreSQL: no superuser application accounts; do not install
dblinkor untrusted PLs; withholdpg_execute_server_programand the file-access roles. - MySQL:
secure_file_priv=NULL; noFILEgrant; no multi-statement drivers. - Oracle: revoke
EXECUTEonUTL_HTTP,UTL_FILE,DBMS_SCHEDULERfromPUBLIC; restrictive network ACLs. - All: run the database as an unprivileged OS user with no outbound network access. Egress filtering alone neutralises every out-of-band technique.
And suppress database errors to clients — log detail server-side with a correlation ID, return a generic message. This does not fix anything, but it downgrades a fast error-based extraction to a slow blind one and removes a fingerprinting aid.
Monitoring and Rate Limiting
Detection does not prevent injection, but blind extraction is inherently high-volume and noisy, which makes it detectable — and detection buys response time.
Worth alerting on:
- Volume-plus-latency — many similar requests to one parameter, some unusually slow. The signature of time-blind or boolean-blind extraction.
- Database errors in application logs — a spike is often someone probing.
sp_configure/ALTER ROLE/GRANTand other configuration changes from the application account — it should never issue these.- Outbound connections from a database host, especially DNS with long high-entropy labels or outbound SMB — the OOB signature.
- Unusual query shapes —
UNION,information_schema, or stacked statements in query logs.
Rate limiting caps extraction throughput and forces an attacker to be slow enough to notice. Neither is a fix; both shorten the window between exploitation and response.
WAF — Honest Expectations
A WAF blocks unsophisticated automated attacks and provides a detection signal. It is a reasonable outer layer.
It is not a fix. Every technique in WAF Bypass exists because pattern-matching HTTP cannot reliably predict what a database will parse. A WAF in front of a parameterised query is defence in depth; a WAF in front of a concatenated query is a speed bump with a false sense of security attached.
If you run one: normalise input the same way your application stack does before matching, prefer positive (allowlist) models over blocklists, block rather than strip-and-forward, and alert on the distinctive encoding permutations bypass attempts produce. Treat it as instrumentation and a delay, never as the control that lets you skip parameterising.
What Each Layer Buys
| Layer | Prevents injection? | What it actually does |
|---|---|---|
| Parameterised queries | Yes | The fix. Removes the vulnerability entirely. |
| Least privilege | No | Caps severity — turns RCE/file access into mere disclosure. |
| Identifier allowlist | Yes, for identifiers | Covers the one context parameterisation cannot. |
| Surface reduction | No | Closes specific escalation techniques. |
| Error suppression | No | Downgrades error-based to blind; slows the attacker. |
| Egress filtering | No | Neutralises out-of-band exfiltration. |
| Monitoring / rate limit | No | Detection and throughput cap; buys response time. |
| WAF | No | Blocks naive automation; signal; bypassable. |
| Input validation | No | Reduces surface; never sufficient alone. |
Making It Stick
Point controls, not culture, are what keep this fixed as the codebase changes:
- Static analysis in CI that flags string-built SQL (Semgrep, CodeQL, Brakeman, Bandit, SpotBugs). Fail the build, do not just warn.
- A banned-methods list —
$queryRawUnsafe,${}in MyBatis mappers,FromSqlRawwith interpolation,where("…#{}")— enforced by lint. - Code review that follows data, asking where each value goes, which is the only way to catch second-order flaws.
- A parameterised-query standard in the codebase, so the safe form is the default anyone reaches for.
- Periodic testing using the methodology here, including the API and second-order surfaces scanners miss.
The fix is cheap and total. The layers exist because the fix is applied by people, and people miss things — so build the net, but never mistake the net for the fix.
Related
A repeatable order of operations for finding and confirming SQL injection: map the surface, probe every parameter, fingerprint, extract the minimum to prove impact.
Where your input lands in the query determines which payloads can possibly work. Get the context wrong and every payload fails for the wrong reason.
The routes from an injected query to a shell on the database host, and which engines hand it to you directly.
Turning a database query into filesystem access: reading configuration and credentials, and writing a webshell into a served directory.
Web application firewalls match patterns; databases parse grammar. Every gap between those two is a bypass.
Input that is stored safely and then used unsafely somewhere else entirely. The payload and the vulnerable query live in different requests, different code paths, and often different applications.