SQLi in Modern Stacks
ORMs and NoSQL databases made classic string-concatenated SQLi rarer — but, exactly like framework auto-escaping did for XSS (the "less, not gone" story), they didn't eliminate injection. They moved it. This chapter covers where injection hides in ORM-heavy code and the NoSQL cousin that catches teams who think "we don't use SQL, so we're safe."
ORMs: Safe by Default, Until the Escape Hatch
ORMs (Sequelize, Prisma, Hibernate/JPA, Django ORM, ActiveRecord, Eloquent) generate parameterized queries automatically for normal operations — which is why ORM code has far less SQLi. The danger is the raw-SQL escape hatches every ORM provides, where you drop to hand-written SQL and the auto-parameterization stops:
dangerouslySetInnerHTML. Search for: sequelize.query / QueryRaw, $queryRawUnsafe (Prisma), .raw() / extra() (Django), find_by_sql / string conditions (ActiveRecord), createQuery with string concatenation (Hibernate/JPQL), and DB::raw / whereRaw (Laravel). Each is a spot where a developer left the safe default — and where injection re-enters if user input is concatenated rather than passed as a binding.
ORM Pitfalls Beyond Raw Queries
Even without dropping to raw SQL, ORMs have sharper edges than "always safe":
- Concatenating into a raw fragment —
whereRaw("age > " + input)is injectable even though it's "using the ORM." - Dynamic column/table/order names — ORMs can't parameterize identifiers either (Chapter 7); passing a user-chosen column/sort needs an allowlist, or it's injectable.
- Operator/structure injection — some ORMs build queries from objects; if you spread untrusted JSON straight into a
whereclause, an attacker may inject operators (this overlaps with NoSQL injection below). - "LIKE" and wildcard handling — user input in a
LIKEneeds its wildcards (%,_) handled, separate from SQLi but a related correctness/abuse issue.
NoSQL Injection: The Cousin That Surprises People
"We use MongoDB, so SQL injection doesn't apply." True for SQL injection — but NoSQL databases have their own injection class, the same root cause (untrusted input changing query structure) in a different query language. The classic is operator injection in MongoDB:
{$ne: null}), and because the code spread the request body straight into the query, that operator became part of the query's logic. That's structurally identical to SQLi: input crossing the boundary from data into query semantics. Other MongoDB vectors include $where (which can run JavaScript) and $regex for inference (a NoSQL analogue of blind extraction). The mental model from Chapter 1 transfers exactly — only the syntax differs.
Defending NoSQL
- Validate types & structure — ensure
passwordis a string, not an object; reject request fields that are objects/arrays where a scalar is expected. This kills operator injection at the source. - Cast/coerce inputs — explicitly treat values as the type you expect (
String(req.body.password)) before they reach the query. - Avoid dangerous operators — disable/forbid
$whereand JavaScript execution; don't pass user input into$regexunescaped. - Use the driver/ODM safely — Mongoose schemas enforce types (a String field rejects an object); don't bypass them with loose queries built from raw request bodies.
The Persistent Lesson
Across ORMs and NoSQL, the same pattern from the XSS course recurs: modern tools make the safe path the default, which slashes injection rates, but every escape hatch and every "spread untrusted input into the query" is where it returns. The defences also rhyme — keep untrusted input as data, never let it become query structure: parameterize raw SQL, allowlist identifiers, and validate types so a JSON object can't sneak in where a string belongs. The framework helps; it doesn't absolve you of the data-vs-code discipline.
Hands-On Exercises
You're security-reviewing an ORM-based codebase. Explain why normal ORM calls are safe and list the specific raw-query methods/patterns you'd grep for across a few ORMs. Then give a "looks like ORM but is injectable" example and its fix.
📄 View solutionExplain MongoDB operator injection using the login example. Show how { "password": { "$ne": null } } bypasses authentication, why it's the same root cause as SQLi, and write the type-validation fix that stops it.
A team says "we switched to an ORM and MongoDB, so injection is no longer a concern." Rebut this, naming the residual risks in both (ORM raw/identifier/operator injection; NoSQL operator/$where/$regex), and state the unifying principle and defences that still apply.
📄 View solutionChapter 9 Quick Reference
- ORMs & NoSQL made classic SQLi rarer, not gone — same "less, not gone" story as XSS framework escaping
- ORMs parameterize by default; injection returns through raw-SQL escape hatches (concatenation in
sequelize.query,$queryRawUnsafe,.raw(),whereRaw,find_by_sql,DB::raw) - Audit ORM code by grepping the raw-query methods (like XSS's escape hatches)
- Other ORM pitfalls: concatenating into raw fragments, dynamic identifiers (need an allowlist), operator/structure injection, LIKE wildcards
- NoSQL injection — same root cause, different language; MongoDB operator injection:
{ "password": { "$ne": null } }= always-true → auth bypass - Other Mongo vectors:
$where(runs JS),$regex(blind-style inference) - Defend NoSQL: validate types/structure (password must be a string, not an object), coerce inputs, forbid
$where, use schema enforcement (Mongoose) - Unifying rule: keep input as data, never query structure — parameterize raw SQL, allowlist identifiers, validate types
- Next chapter: testing, tooling (sqlmap) & the hardening checklist — the course finale