How to fix NoSQL Injection
MongoDB, CouchDB, DynamoDB and friends don't have SQL — but they have query operators ($gt, $ne, $regex, $where) that attackers can inject the same way. Auth bypass via { '$ne': null } is one of the easiest exploits in modern web apps. Here's the complete fix.
What is NoSQL Injection?
NoSQL injection happens when user input flows into a NoSQL query without being treated as a value. The classic Express + MongoDB pattern: `User.findOne({ username: req.body.username, password: req.body.password })`. If the body is JSON `{"username":"admin","password":{"$ne":null}}`, MongoDB matches the first user where username='admin' and password is not null — bypassing auth entirely without knowing the password.
The vulnerability is fundamentally an *input-type* issue: the query expects strings, the input is an object containing operators. SQL injection has the same root cause (control characters interpreted as syntax) but with NoSQL the attack surface is JSON object structure, not string concatenation.
Common attack vectors: `$ne` (not equal — auth bypass), `$gt`/`$lt` (range operators — enumeration), `$regex` (slow regex DoS or blind injection), `$where` (server-side JavaScript execution in MongoDB <5.0 — can be RCE), `$elemMatch` (array index queries), `$in` (returning more documents than intended). Modern MongoDB 5.0+ disables `$where` by default but old versions and many ODMs still process it.
What an attacker can do
The concrete impact of leaving NoSQL Injection unpatched.
Auth bypass without knowing credentials
`{ "username": "admin", "password": { "$ne": null } }` logs in as admin if any password is set.
Mass data extraction via blind injection
`$regex` patterns reveal field values one character at a time via response timing or boolean differences.
RCE via $where (MongoDB <5.0)
`$where` accepts JavaScript expressions evaluated server-side. Carefully crafted input becomes JS execution.
ReDoS via attacker-controlled $regex
Crafted regex patterns (`(a+)+$`) take exponential time, blocking the worker thread.
How do I know if I'm vulnerable?
Manual: search your codebase for query objects built from request input — `User.find({ ...req.body })`, `db.collection.find(req.query)`, ODM `where(req.params)` calls. Each one needs validation that input fields are strings, not arbitrary objects.
Automated: AuditCore's NoSQL Scanner tries `$ne`, `$gt`, `$regex`, `$where`, and `$or` payloads in JSON bodies and query params. Detection: response-time differences, response-size differences, or successful auth with crafted operators. Static analysis via Semgrep flags spread/destructure-from-request patterns.
How to fix NoSQL Injection
5 ordered steps. Apply them in order — each builds on the previous.
- 1
Cast input fields to expected types before querying
If the field should be a string, force it to a string. If it should be a number, parse it. Reject objects.
Express middleware like `express-mongo-sanitize` strips keys starting with `$` or containing `.`. ODMs like Mongoose enforce schema types when you use schema-validated methods.
javascriptStep 1// ❌ Vulnerable — accepts any input shape app.post("/login", async (req, res) => { const user = await User.findOne({ username: req.body.username, password: hashed(req.body.password), }); if (user) res.json({ ok: true }); }); // ✅ Force input to strings app.post("/login", async (req, res) => { const username = String(req.body.username ?? ""); const password = String(req.body.password ?? ""); if (!username || !password) return res.sendStatus(400); const user = await User.findOne({ username, password: hashed(password), }); if (user) res.json({ ok: true }); }); // ✅ Or use express-mongo-sanitize as middleware import mongoSanitize from "express-mongo-sanitize"; app.use(mongoSanitize()); - 2
Use schema-validated ORMs/ODMs (Mongoose, Prisma)
ODMs with schemas reject objects where strings are expected at the type level — eliminating most NoSQLi paths.
Mongoose: define types in schema; `find({ username })` casts. Prisma's MongoDB connector type-checks at compile time. Skip raw `db.collection.find()` calls when possible.
javascriptStep 2import mongoose from "mongoose"; const UserSchema = new mongoose.Schema({ username: { type: String, required: true, index: true }, password: { type: String, required: true }, role: { type: String, enum: ["admin", "member"], default: "member" }, }); const User = mongoose.model("User", UserSchema); // Mongoose casts username to String. Object input throws a CastError. const user = await User.findOne({ username: req.body.username }); - 3
Validate request bodies with a schema (Zod, Joi, AJV)
Reject the request before it reaches the query layer. Schemas describe exactly what shape input should have; anything else is rejected.
Zod is the modern TypeScript-friendly choice. Add it to every route — it documents the API as a side effect.
typescriptStep 3import { z } from "zod"; const LoginSchema = z.object({ username: z.string().min(1).max(64), password: z.string().min(1).max(128), }); app.post("/login", async (req, res) => { const parsed = LoginSchema.safeParse(req.body); if (!parsed.success) return res.status(400).json({ error: "Bad request" }); const { username, password } = parsed.data; const user = await User.findOne({ username, password: hashed(password) }); res.json(user ? { ok: true } : { error: "Invalid credentials" }); }); - 4
Disable $where and server-side JavaScript
MongoDB's `$where` accepts JavaScript that runs on the server. Disable it at the database level.
MongoDB 5.0+ disables it by default; older versions need `--noscripting` flag or `setParameter` config. If you must use `$where`, never include user input.
yamlStep 4# mongod.conf — disable JS execution security: javascriptEnabled: false # Or at runtime (requires admin): # db.adminCommand({setParameter: 1, allowDiskUse: false}) # In your code, NEVER do this: # db.users.find({ $where: `this.score > ${userInput}` }) # If you need scoring/sorting, use aggregation pipeline: # db.users.aggregate([{ $match: { score: { $gt: parseInt(userInput, 10) }}}]) - 5
Limit regex inputs to prevent ReDoS
If users can supply regex patterns (search features), use a regex engine with execution-time limits, or compile their pattern with anchors and length limits.
Node's V8 regex is vulnerable to ReDoS. Use `re2-wasm` for safe regex (Google's RE2 doesn't backtrack). For simple search, use `$text` indexes or `$regex` with strict caps on input length and complexity.
javascriptStep 5// ❌ User regex passed straight to MongoDB const results = await Posts.find({ title: { $regex: req.query.q } }); // ✅ Escape user input as literal string in regex function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } const safe = escapeRegex(String(req.query.q ?? "")).slice(0, 100); const results = await Posts.find({ title: { $regex: safe, $options: "i" }, }); // ✅ Better: use a text index // Posts.createIndex({ title: "text" }) // Posts.find({ $text: { $search: req.query.q }})
How to verify the fix
Run AuditCore — the NoSQL Scanner sends `{"$ne":null}`, `{"$gt":""}`, `{"$regex":"."}`, and `$where` payloads in both JSON bodies and URL params. Findings include the payload that worked plus the response that confirmed it.
Manual: in your login form, with curl: `curl -X POST -H 'Content-Type: application/json' -d '{"username":"admin","password":{"$ne":null}}' https://app.example.com/login`. If you log in as admin without a password, you have NoSQLi. Repeat with operator combinations on every endpoint accepting JSON.
Frequently asked questions
Is NoSQL injection less severe than SQL injection?+
Comparable. NoSQLi auth-bypass is often easier to exploit than SQLi (`{"$ne":null}` works without knowing schema), and `$where` in older MongoDB enables RCE just like stacked SQL queries do. The defense surface is smaller (operators are predictable) but the impact is similar — full data access, auth bypass, in some cases RCE.
Does using an ORM/ODM make me immune?+
Mostly, IF you use schema-validated methods. Mongoose's `find({ username })` casts to String. But Mongoose's `find()` accepts arbitrary query objects — if you pass `{ username: req.body.username }` and `req.body.username` is `{$ne:null}`, Mongoose still passes it through. Use either schema casts (Mongoose lean queries) or always cast input with `String()`.
Should I use express-mongo-sanitize globally?+
Yes — it's defense-in-depth. The middleware strips keys starting with `$` and containing `.` from req.body, req.params, req.query. Combined with schema validation it gives you two layers. Caveat: if your app legitimately uses dotted notation (rare) you'll need to configure exemptions.
Does NoSQL injection apply to DynamoDB / CosmosDB / Firestore?+
Yes for any document/key-value store with operator syntax. DynamoDB conditional expressions, CosmosDB SQL queries (which are SQL-like, so SQLi rules apply), Firestore query constraints — each has its own injection variants. Same defense pattern: parameterized queries / typed values / no raw user input in operators.
Can I rely on Content-Type: application/json validation?+
No. The attacker controls Content-Type. Even when you receive JSON, the body might be `{"username":"admin","password":{"$ne":null}}` — perfectly valid JSON. The fix is type validation of the parsed object, not Content-Type checking.
Related fix guides
How to fix SQL injection
Same OWASP A03 family. Parameterized queries instead of string concat — covered for Node/Python/Go/PHP/Ruby/Java.
Read guideHow to fix XSS
Output-side equivalent of injection. Both share the principle: validate input, escape output, enforce types.
Read guideHow to fix business logic abuse
NoSQLi-induced auth bypass is a business logic flaw. Both need defense beyond syntax-level validation.
Read guide