skip to content

Teams often say "we use an ORM" or "all access goes through stored procedures, so injection is impossible." Under what conditions is each claim false, and does the same reasoning carry over to document-database query languages?

level: seniorimportance: should knowfreq 52%

answer

  1. Safety belongs to the assembly point, not the layer's name
  2. ORM escape hatches: native SQL, string fragments, JPQL/HQL, IN-lists
  3. Procedure body EXEC(@sql) = injectable, often with definer rights
  4. Procedure source escapes code review and static analysis
  5. {"$ne": null} = OR 1=1 without a parser → enforce types

basics

~20 s

Both claims describe a tool, not the invariant. An ORM is safe only where it builds the statement; string fragments and native-query escape hatches are not. A procedure is safe only if its body does not itself assemble dynamic SQL from its arguments. Document databases have the same defect in operator form.

solid answer

~50 s

Safety is a property of *how the query text is assembled*, never of the layer's name. ORMs are safe for generated statements, but every ORM ships escape hatches — raw/native SQL, string fragments in a `where("...")` call, string-built JPQL/HQL, hand-spliced IN-lists and LIKE patterns, and user-chosen field names in dynamic sorting. Injection into the ORM's own query language is still injection: it can read other entities even if it cannot drop tables. Stored procedures move the assembly point but do not remove it: a body using `EXEC(@sql)` or `EXECUTE IMMEDIATE` on concatenated arguments is injectable, and it often runs with the definer's higher privileges, so exploitation is worse. Document stores show the structural version: substituting an object where a scalar was expected (`{"$ne": null}`), or server-evaluated expressions, confuses data with instructions without any string parser at all — so the defence is type-enforcing the input, not escaping it.

code

text · 7 lines
text
SQL          WHERE name = '" + v + "'          -> v becomes syntax
Object query  "from User u where u.n = '" + v   -> v becomes ORM-language syntax
Document      query.password = parse_json(v)    -> v becomes an OPERATOR NODE
              expected: "hunter2"   sent: {"$ne": null}

defence in the first two: bind values, allowlist identifiers
defence in the third:     assert typeof(v) == scalar string BEFORE it is a node

go deeper

for a junior

Say that safety depends on how the query text is built, and give one concrete escape hatch for each: raw queries in an ORM, dynamic SQL inside a procedure.

for a middle

Add injection into the ORM's own query language and the fact that document stores have an operator-shaped equivalent.

for a senior

Bring in definer's rights, the invisibility of procedure source to code review and static analysis, and why type enforcement rather than escaping is the fix for structured query languages.

for a principal

State the invariant once and use it to evaluate any new data-access technology, then talk about concentrating and linting the escape hatches so the risk surface is enumerable.

## The invariant, restated Injection is possible wherever an interpreter receives structure and data through one channel and cannot tell which is which. The defence is that *structure comes from trusted code and values arrive through a path that cannot alter structure*. Every claim of the form "technology X makes injection impossible" should be tested against that sentence, not accepted on the technology's reputation. ## ORMs What an ORM genuinely gives you is that, for statements it generates from a typed model, the generated text is fixed by the mapping and the user's values are bound. That covers a large share of a typical codebase, which is why ORM-heavy applications really do have fewer injection defects. But it is a coverage claim, not a guarantee, and four escape hatches recur: 1. **Native/raw query APIs.** Every mature ORM offers one, because reporting and vendor-specific features demand it. Inside that call the ORM contributes nothing; it is string concatenation with extra steps. 2. **String fragments in the builder.** Interfaces that accept a condition as text (`where("status = '" + s + "'")`) or an ordering as text are pass-throughs. The surrounding builder is safe, the fragment is not, and reviewers relax because the call looks like ORM code. 3. **Injection into the ORM's own query language.** Object query languages such as JPQL or HQL are parsed by the ORM, so a concatenated fragment is injectable there too. The blast radius is different — typically no DDL and no stacked statements — but an attacker can still pivot across associations to read entities they are not entitled to, which is the exfiltration half of the classic threat. 4. **Shapes the API cannot bind.** IN-lists built by joining strings, LIKE patterns where the wildcards are not escaped separately from the quotes, and dynamic sort/filter columns — the identifier problem again. So the honest statement is: an ORM raises the floor and concentrates risk into a small, greppable set of call sites. Treat those call sites as security-relevant, review them, and lint for them. ## Stored procedures The common belief is that a procedure is a parameterised call, therefore safe. The call *interface* is indeed parameterised; the question is what the body does with those parameters. A body that runs a static statement referencing the parameters is safe for the same reason binding is safe. A body that constructs SQL text — `EXEC(@sql)` in T-SQL, `EXECUTE IMMEDIATE` in PL/SQL, `EXECUTE ... USING` in PL/pgSQL, `PREPARE`/`EXECUTE` in MySQL — by concatenating its arguments is exactly as injectable as the application would have been. Dynamic SQL inside procedures is common precisely because procedures are where teams put the flexible reporting logic. Two things make this worse than the application-side equivalent. First, privilege: procedures frequently execute with the definer's rights, so the injected statement runs as a more powerful principal than the calling service's own account, defeating the least-privilege mitigation you were relying on. Second, visibility: the source lives in the database rather than the application repository, so it escapes code review, secret scanning, static analysis and often version control. The mitigation is identical to the application case — use the engine's parameterised dynamic execution form (`sp_executesql` with typed parameters, `EXECUTE IMMEDIATE ... USING`) so values bind, and allowlist identifiers — plus running procedures with invoker's rights unless a specific escalation is intended. ## Document and other non-SQL query languages Dropping SQL does not drop the bug class; it changes its clothes. Two distinct forms appear. **Operator/structure injection.** Many document stores accept a query as a structured object where a field maps to either a scalar (equality) or an object (an operator). If a request body is deserialised straight into that query object, an attacker who sends `{"password": {"$ne": null}}` where a string was expected converts "equals this password" into "is not null", the direct analogue of `OR 1=1`. No string parsing occurred anywhere; the confusion is between a value and a structural node. The defence is therefore not escaping — there is nothing to escape — but *type enforcement*: assert the field is a scalar of the expected type before it is placed in the query, and never build a query by copying user-supplied maps wholesale. **Server-evaluated code.** Query features that evaluate an expression or script server-side reconstitute a full interpreter boundary and are injectable in the classic string sense; they should be disabled or never fed untrusted text. The same reasoning applies to LDAP filters, XPath expressions, template engines and any ORM-like layer over a graph query language: identify the interpreter, identify the channel the value takes, and ask whether that channel can express structure. ## The answer an interviewer wants Refuse the framing that a technology confers safety. State the invariant, then for each technology say where its assembly point is and what escapes it: ORMs — raw queries, string fragments, object-query-language concatenation, unbindable identifiers; procedures — dynamic SQL in the body, made worse by definer privileges and poor visibility; document stores — operator injection defended by type enforcement rather than escaping.

  • A stored procedure builds dynamic SQL because the caller may filter on any of twenty optional criteria. How do you make it safe without losing the flexibility?
    Keep the dynamic assembly but make every user value a bound parameter of the dynamic execution form — `sp_executesql` with typed parameters, or `EXECUTE IMMEDIATE ... USING` — so the text you concatenate contains only developer-written predicates plus placeholders. Any identifier that varies comes from an enumerated map inside the procedure. Also run the procedure with invoker's rights unless escalation is deliberately required, so an exploit does not inherit the definer's privileges.
  • Why is escaping the wrong instinct for operator injection in a document store?
    There is no string being lexed, so there are no metacharacters to neutralise — the attacker supplies a different *node type* in a structured query, turning a scalar comparison into an operator. Escaping a string that will never be parsed changes nothing. The correct control is to validate and coerce the input to the expected scalar type before it becomes part of the query object, which is the structural-separation rung applied to a structured protocol.

saying these in an interview costs you the question

  • "We use an ORM, so we cannot have injection" — ignores raw-query and string-fragment escape hatches.
  • "Injection into the ORM's query language is harmless because you cannot DROP anything" — exfiltration across associations is the more common real impact anyway.
  • "Stored procedures are parameterised by definition" — the call is; the body may still concatenate.
  • "NoSQL is immune because there is no SQL" — operator and server-side-expression injection are the same defect in structured form.
  • Assuming least-privilege database accounts still contain a procedure running with definer's rights.

context