SQL injection is usually taught as a quoting problem — "the apostrophe breaks out of the string literal." Give a definition of the bug class that also explains an unquoted numeric predicate such as `WHERE id = <input>`, and state the properties a call site must have for the defect to exist at all. Why is "block the dangerous characters" the wrong frame?
answer
- Content decides the role
- One channel, two authorities
- Interpreter more expressive than the intent
- Numeric predicate: no quote, still injected
- Rows returned ≠ credentials verified
basics
~20 sInjection exists wherever untrusted input reaches an interpreter through a channel in which the input's own content decides its grammatical role. Two properties make it possible — one channel for instructions and data, and an interpreter more expressive than the intended operation. Characters are incidental.
solid answer
~50 sDefine it by **role assignment**, not by characters: an injection defect exists wherever untrusted input reaches an interpreter through a channel in which the input's own content decides its grammatical role. Two properties are required. (1) One channel carries the programmer's instructions and the user's data with no out-of-band marking of which is which. (2) The interpreter downstream is strictly more expressive than the operation intended — you wanted an equality comparison; the grammar offers predicates, subqueries, set operations and comments — so injection is escalation inside a grammar. A third property is not required but multiplies impact: a caller that trusts result *shape* over result *meaning*. `' OR '1'='1` is the familiar illustration, but it only authenticates because code read "a row came back" as "credentials verified". The role definition also covers `WHERE id = 7 OR 1=1`, where no literal is ever opened — which any character-based definition misses.
code
text · 13 linesintended roles actual roles at the sink
------------------------------- ------------------------------------
WHERE name = <VALUE> VALUE -> literal ... then PREDICATE
(input content opened a new node)
WHERE id = <VALUE> VALUE -> number ... then PREDICATE
input: 7 OR 1=1 (no literal delimiter involved at all)
filter field = <VALUE> VALUE -> operator instead of scalar
(structured protocol) (nothing lexed; the node type changed)
invariant: wherever the content of the input picks its own role,
the defect exists — the character set is incidental.go deeper
State the definition in one sentence — the input's content decides its grammatical role — give the familiar always-true predicate as the illustration, and say the fix is separating structure from values. Do not define the bug as "the apostrophe".
Give both required properties plus the amplifier, and say where role assignment happens (lexing and parsing, before any evaluation). Prove the definition with the unquoted numeric case.
Use the definition as a review instrument: enumerate sinks, rank by how expressive the interpreter is and what the executing principal may do, and call out the shape-over-meaning defect as separately fixable. Be explicit that character filtering is a rung on the ladder, not the ladder.
Frame it as one invariant holding at every interpreter boundary the system owns — query languages, directory filters, expression evaluators — and argue for making the safe construction the only construction available, so the property holds by default instead of by review attention.
## The definition that survives the missing quote The usual teaching example is a quoted string: an apostrophe in the input ends the literal early and the rest of the input is read as predicate syntax. That is a true *instance*, and the mechanics of it belong to the applied tier. It is a bad *definition*, because the bug class outlives the quote. Use this instead: **an injection defect exists when untrusted input reaches an interpreter through a channel in which the input's own content decides the input's grammatical role.** The developer intends the bytes to occupy one role — a leaf of the syntax tree, a value compared against a column. The interpreter assigns roles by reading bytes, so bytes shaped like structure become structure. Test the definition against cases the character frame fails: - `WHERE id = <input>` with `7 OR 1=1`. No literal is opened, no apostrophe appears, and the predicate is still widened to every row. - A sort or limit position, where the value was never a literal to begin with. - A structured (non-textual) query protocol where a filter field arrives as an operator object rather than a scalar: nothing is lexed at all, yet the role moved from "value" to "operator". Escaping has no meaning there, because nothing is being read as text. - The same shape in other grammars: an LDAP search filter, where input closing and reopening parentheses turns one predicate into a boolean expression; an XPath expression, where input becomes a new step or predicate in the location path. One definition covers all of them. "Dangerous characters" covers none of them completely. ## Property 1 — one channel, two authorities A query built as text is a single channel carrying two kinds of authority: what the program commands, and what the user supplied. Nothing in the transmitted bytes records the difference; the boundary existed only in the source code and was destroyed when the two were joined. Any single channel that must still express the distinction needs an in-band marker — quotes, delimiters, prefixes — and any in-band marker can be forged by whoever controls the data. This is a general property of shared channels, not a property of SQL. ## Property 2 — an interpreter more expressive than the intent The operation intended is "compare this value for equality". The grammar available at the sink can express joins, subqueries, set operations, comments, and — where the transport permits — more than one statement per call. Injection is escalation *inside a grammar*: the attacker moves from a leaf node to an arbitrary node. This is why sinks with an identical defect differ enormously in impact: what an attacker gets is roughly what the grammar can express, bounded by what the executing principal is permitted to do. It also explains why character filtering keeps losing. A character filter defends a *language* boundary with a *lexeme* rule; the language is defined by the interpreter, not by the filter's author. ## Property 3 (not required, but an amplifier) — shape over meaning Even a fully widened predicate does not authenticate anyone by itself. It authenticates when the caller treats "at least one row was returned" as "the credential was verified". That is a second, independent defect, and it is the one that turns a data-disclosure bug into an authentication bypass. Code that fetches the stored credential material for the named user and verifies the supplied secret against it refuses the login even while the query is returning the whole table. The two defects are separable: fixing either one alone leaves the other standing. ## Where the role is actually decided An engine processes a statement in stages: a lexer splits characters into tokens (identifiers, literals, operators, comments), a parser assembles tokens into a syntax tree, and only then does a planner choose an execution strategy and the executor evaluate anything. Role assignment is finished at the first two stages, before a single semantic decision is made. Two consequences follow. First, the engine cannot help you: by the time evaluation begins, the injected node is an ordinary, well-formed, fully authorised part of the tree, indistinguishable from a node the developer wrote. There is no "malicious query" for it to refuse — the query is valid and the caller is entitled to run it. Second, any defence must act *before* the text is lexed. That is precisely what structural separation means operationally: the statement's shape is fixed while the values are still absent, so no value can ever be promoted to structure. ## Why the character frame is the wrong frame The defence ladder, strongest to weakest, is: **structural separation > escaping/transformation > validation > detection**, weakening from a guarantee to a heuristic at each step down. Take one adjacent pair as the reason the ordering is real: validation beats detection because validation refuses at the boundary against a *closed-world* set the code itself owns and can enumerate, while detection must recognise hostile-looking input in an *open world* whose contents the attacker chooses. Character blocking is an open-world rule wearing closed-world clothing: it enumerates what it believes is bad, and it must also silently model the interpreter's grammar to know what "bad" even is. It fails in both directions. It misses unquoted contexts, structured-protocol role changes, and any grammar whose dangerous constructs are not the ones on the list. It also corrupts legitimate data — names with apostrophes, free text, search terms — which pushes teams to loosen the filter until it stops protecting anything. ## Using the definition as a review tool At each sink ask three questions. Does the input's content decide its grammatical role? Is the interpreter more expressive than the operation intended? Does the caller act on the shape of the result rather than its meaning? Yes to the first two means injectable, regardless of which characters happen to be involved; yes to the third means the severity multiplies. ## Interview framing Lead with the role-assignment definition. Prove it with the case that has no quote. Name the two required properties and the caller-side amplifier. Close by naming the fix as fixing the structure before values exist — a category difference from filtering, not a stronger filter.
- A team argues that an endpoint parameter is safe because the field is documented as an integer id. Is that argument sound?Only if the integer type is actually enforced before the value reaches the statement — parsed into a numeric type, or bound as a parameter — so that the value can never occupy a structural role. If the check is a shape inspection on a string that is then joined into the statement text, the value's content still decides its role, and any input that survives the check is injected. The soundness rests on structural enforcement, not on the documented type.
- Apply the three-question test to a sink that is not a relational database.An LDAP search filter qualifies: the filter string carries both the program's predicate and the user's value on one channel, and the filter grammar is far more expressive than the equality test intended — closing and reopening parentheses lets input add a disjunction. An XPath expression is the same shape, where input becomes an extra step or predicate in the location path. Both are frequently paired with the shape-over-meaning defect, since callers read "an entry/node was returned" as "the user is authentic".
- Why can't the database engine just reject the injected statement?Because role assignment is complete at lex and parse time, before evaluation. What reaches the planner is a well-formed, fully authorised syntax tree whose injected node is indistinguishable from one the developer wrote. The engine has no record of which characters came from the program and which from a user; that information was destroyed when the text was assembled.
saying these in an interview costs you the question
- "It's the apostrophe" — a numeric predicate, a sort position, or an operator-shaped filter value is injected with no quote character anywhere.
- "The database should refuse malicious queries" — the injected node is valid, authorised syntax by the time the engine evaluates anything; there is nothing for it to detect.
- Defining the class by a blocklist of characters or keywords, which is open-world by construction and must silently reproduce the interpreter's grammar to work.
- Treating the login bypass as one bug — the widened predicate and the caller that reads row count as proof of identity are two independent defects.
- Assuming input from an authenticated or internal caller is not untrusted; the property that matters is who controls the content, not where it entered.