Claude on Question 19, Sep 22
Explain the general classes of SQL injection so I can brief my developers.SQL Injection: A Briefing Taxonomy
Root cause (lead with this)
Every SQL injection has the same origin: untrusted data is concatenated into a query string, so the database parser interprets attacker data as code. Everything else is detail. If your developers internalize only one thing, it's that queries must be structured before data is supplied, never assembled from data.
# Vulnerable — data becomes code
cursor.execute("SELECT * FROM users WHERE email = '" + email + "'")
# Safe — structure is fixed, data is bound
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))There are three useful ways to classify SQLi. Developers benefit most from #2 and #3; pentesters and detection engineers care most about #1.
1. Classification by how the attacker retrieves data
In-band (results come back through the same channel)
Union-based — the attacker appends UNION SELECT to append rows from other tables into the application's own output.
id=1 UNION SELECT username, password_hash FROM users
Requires matching column counts/types; attackers enumerate these quickly.
Error-based — the attacker forces the DB to emit an error message containing data. Classic on SQL Server (CONVERT, CAST) and MySQL (EXTRACTVALUE, UPDATEXML). Any stack trace or raw DBMS error reaching the user is both an oracle and a data channel.
Inferential / "blind" (no data returned, only inference)
Boolean-based — the attacker injects a condition and observes a binary difference in the response (200 vs 500, content present vs absent, subtle length change).
id=1 AND SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a'
One bit at a time, but fully automated by tools — a "blind" hole is not a lesser hole.
Time-based — inference from response latency when there's no observable content difference: SLEEP(), pg_sleep(), WAITFOR DELAY, or heavy-computation equivalents.
Out-of-band
Exfiltration through a secondary channel the attacker controls — DNS lookups or HTTP requests triggered from the database (xp_dirtree, UTL_HTTP, LOAD_FILE against UNC paths). Relevant because it bypasses response-based detection entirely and works even in fully asynchronous flows. Mitigation includes egress filtering from DB hosts.
2. Classification by injection context (most actionable for developers)
This is where the "just use prepared statements" advice needs nuance, because placeholders only work where the SQL grammar allows a value.
| Context | Parameterizable? | Correct approach |
|---|---|---|
String literal in WHERE | Yes | Bind parameter |
| Numeric comparison | Yes | Bind parameter |
LIKE pattern | Yes, but escape % _ | Bind, plus escape wildcards to prevent DoS/over-matching |
IN (...) list | Yes, with generated placeholder count | IN (?,?,?) built from list length, values bound |
| Table / column name | No | Allow-list mapping from input token → hard-coded identifier |
ORDER BY column and direction | No | Allow-list; map "name_asc" → "name ASC" |
LIMIT / OFFSET | Usually yes on modern drivers | Bind; otherwise cast to integer |
| Schema/DDL statements | No | Avoid user influence entirely |
The ORDER BY case is the single most common real-world bypass of an otherwise parameterized codebase, and it is the hardest to spot in review because the surrounding code looks safe.
// Vulnerable despite prepared statements elsewhere
String sql = "SELECT * FROM orders ORDER BY " + sortField + " " + direction;
// Safe — allow-list
Map<String,String> SORTS = Map.of("date","created_at DESC", "total","total ASC");
String clause = SORTS.getOrDefault(sortKey, "created_at DESC");3. Classification by vector and timing
First-order — input is used in a query during the same request. What scanners find easily.
Second-order (stored) — input is safely stored, then later concatenated into a query by a different code path: a batch job, admin report, cron export, or audit log viewer. A username of admin'-- inserts fine via a parameterized INSERT, then breaks a report that builds SQL by concatenation. Data from your own database is not trusted input. This class routinely survives pentests and is worth emphasizing hard.
Input channel — remind developers that injection isn't only query strings and form fields: HTTP headers (User-Agent, X-Forwarded-For, Referer), cookies, JSON/XML bodies, SOAP, GraphQL variables, filenames, webhook payloads, imported CSVs, and messages from internal queues. Anything logged to a database via string-built SQL is a vector.
Stacked queries — ; DROP TABLE ... where the driver permits multiple statements. Support varies (commonly enabled on SQL Server, often disabled by default for MySQL/PHP), so its absence is a mitigation, not a defense.
4. Adjacent classes worth naming
- •Stored procedure injection — procedures are not inherently safe. A proc that internally does
EXEC('SELECT * FROM t WHERE x=' + @p)is vulnerable. Usesp_executesqlwith typed parameters, or avoid dynamic SQL inside procs. - •ORM / abstraction-layer injection —
HQL/JPQL/ActiveRecordraw fragments,.where("name = '#{params[:n]}'"), Django.extra()/RawSQL, Hibernate string-concatenated HQL. ORMs reduce exposure but the escape hatches are exactly where bugs cluster. Grep for the raw-query APIs in your stack. - •NoSQL / operator injection — MongoDB
{"password": {"$ne": null}},$whereJavaScript evaluation. Same root cause, different grammar. Validate types, reject keys beginning with$. - •JSON path and dynamic filter builders — home-grown "query DSLs" that translate client filter objects into SQL are a frequent modern source.
Defense priorities, in order
- 1.Parameterized queries / prepared statements everywhere as the default and enforced pattern.
- 2.Allow-lists for non-parameterizable elements (identifiers, sort keys, direction).
- 3.Least privilege DB accounts — separate read-only credentials for reporting, no DDL rights for app users, no
FILE/xp_cmdshell. Limits blast radius when #1 fails. - 4.Treat stored data as untrusted in downstream query construction.
- 5.Generic error pages, detailed logging server-side only — removes error-based channels and oracles.
- 6.Input validation as defense-in-depth, never as the primary control. Type/length/format checks are good; escaping and blocklists of keywords are not a strategy.
- 7.Automation in CI — SAST rules for string-concatenated SQL, DAST/
sqlmapagainst staging, plus a code-review checklist item. - 8.WAF and egress filtering as compensating controls only.
Suggested review checklist for your team
- •Grep for string concatenation, interpolation, and format calls adjacent to SQL keywords.
- •Grep for raw-query escape hatches in your ORM.
- •Inspect every
ORDER BY,LIMIT, and dynamic table name. - •Identify queries built inside batch jobs, reports, and log viewers (second-order risk).
- •Confirm DB credentials used by each service have the minimum required grants.
- •Confirm no DBMS errors reach clients.
If it helps, I can turn this into slides, write language-specific safe/unsafe examples for your exact stack, or draft the SAST rules and review checklist as artifacts.
Exact recordShowHideThe model id, the run, the verbatim text with line numbers and its full sha256
- Scored
- answered
- Received
- 2026-09-22 09:13:18 UTC
- Run
- 20260922T090022Z-7ff13b
- Served by
- Claude Platform on AWS · HTTP 200 · finish stop
- Size
- 7,379 characters · 3,448 tokens out incl. hidden reasoning · 50.1 s
End of response · 118 lines · 7,379 characters · sha256 3d577e8c33011db462064676e51cec820b9ee7649aa08e3de6776e06d7df22e1