NoSQL injection
CWE-943OWASP A03:2021Updated August 31, 20266 min read
NoSQL injection is a vulnerability that lets an attacker change the meaning of a database query by sending a query operator where the application expects a plain value, for example $ne or $gt in a JSON body sent to MongoDB. That is enough to bypass a login check or read documents belonging to someone else. The fix is schema validation and casting input to the type you expect.
NoSQL databases such as MongoDB store documents rather than rows in tables, and they do not speak SQL. That does not make the injection problem go away, it only changes its shape: instead of a fragment of SQL, an attacker smuggles in a query operator. Here is how that works, what it gets an attacker, and how to prevent it.
What is NoSQL injection?
NoSQL injection is a vulnerability that lets an attacker change the meaning of a database query by supplying input that the application treats as a plain value but that is in fact a piece of query language. NoSQL stands for “Not only SQL”, an umbrella term for databases that do not use tables and SQL. MongoDB is by far the best known of them.
The difference from SQL injection is structural. A SQL query is a line of text, and an attacker splices text into it. A MongoDB query is not text but a structured object: a set of fields and values, in which a value is allowed to be an operator such as $ne (not equal) or $gt (greater than). As soon as user input lands directly as a value in that object, and the input turns out to be an object itself, the database reads it as an operator rather than as data.
An everyday comparison: at a counter you fill in a form with a surname and a date of birth. Instead of a date, someone writes the sentence “any date will do”. A clerk who copies the form literally into the search system now gets a list of everybody.
That is what makes JSON APIs the natural entry point: an HTML form always yields text, while JSON preserves the difference between a string and an object and passes it to your code intact.
How does a NoSQL injection attack work?
Take a login endpoint in Node.js that receives the request as JSON and drops the two fields straight into a MongoDB query.
Vulnerable:
// The fields in req.body come straight from the caller's JSON
app.post("/api/login", async (req, res) => {
const user = await db.collection("users").findOne({
email: req.body.email,
password: req.body.password
});
if (user) {
req.session.userId = user._id;
return res.json({ ok: true });
}
res.status(401).json({ error: "invalid credentials" });
});
During a normal sign-in the browser sends two strings and this behaves exactly as intended. An attacker, however, sends no strings at all, but two small objects:
POST /api/login HTTP/1.1
Host: app.example.com
Content-Type: application/json
{"email": {"$ne": null}, "password": {"$ne": null}}
The query MongoDB now runs looks for a document whose email is not equal to null and whose password is not equal to null either. That describes virtually every user in the collection. findOne returns the first match, the application sees a user, and a session is created. The attacker is in without a single valid credential. If they fill in the real email address of a known administrator and leave only the password as an operator, they also choose which account they take over.
It does not stop at logging in. With the $regex operator an attacker can recover a password hash, reset code or API key one character at a time, by repeatedly asking whether the stored value starts with a given pattern and reading the answer off the difference in the response. It is the same method as blind SQL injection, built out of different parts.
The fix has two halves that reinforce each other: validate the request against an explicit schema before it reaches the database, and make sure every value has the type you expect.
Secure:
import { z } from "zod";
const LoginBody = z.object({
email: z.string().email().max(254),
password: z.string().min(8).max(200)
});
app.post("/api/login", async (req, res) => {
const parsed = LoginBody.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: "invalid input" });
}
// Only a string ever reaches the query; an object cannot get this far
const email = String(parsed.data.email).toLowerCase();
const user = await db.collection("users").findOne({ email });
if (!user || !(await bcrypt.compare(parsed.data.password, user.passwordHash))) {
return res.status(401).json({ error: "invalid credentials" });
}
req.session.userId = user._id;
res.json({ ok: true });
});
Two things have changed. The schema rejects any request in which the email or the password is not a string, so $ne never gets past the front door. And the password is no longer compared inside the query but afterwards against a hash, which means the database no longer decides who is authenticated. The explicit String() cast is the last guard rail for values that arrive from somewhere else.
?email[$ne]=x turns into a nested object just as readily.What is the impact of NoSQL injection?
Severity depends on where the input lands in the query, and the range is wide. When the flaw sits in an authentication check the outcome is immediate and total: an attacker signs in without a password, as an administrator if they choose, and inherits every permission that account holds. When it sits in a search or filter function the outcome is quieter but no less damaging: a filter meant to return only the caller’s own documents can be widened to the whole collection, including the records of other tenants in a shared environment.
Targeted extraction is the second pattern. A $regex attack lets an attacker reconstruct exactly the fields that are worth the most: password hashes, session tokens, reset codes. If the input reaches an update operation, documents can be modified as well, for instance to raise the attacker’s own role.
At the top of the range sits code execution. Constructs such as $where and mapReduce evaluate JavaScript on the database server, so where those features are enabled an injection can escalate into a full read of the data set or a denial of service through an endless loop. That spread is why the severity runs from high to critical, and why a confirmed case usually triggers breach notification obligations under GDPR or comparable regimes.
How do you detect NoSQL injection?
The core of the testing work is type manipulation: wherever the application expects a string, send an object instead and compare the response. A request in which one field carries an operator such as $ne or $gt and that suddenly produces a successful login, a longer result list or a noticeably slower response is a strong signal. So is a server error carrying a Mongo stack trace, since it proves the input reached the query untouched.
Automated scanners miss this regularly, for an understandable reason: they mutate the text inside a parameter, not its type. A scanner that appends ' OR 1=1 to every value never gets past a rejected string on a JSON API. Code review closes the gap quickly: look for calls to find and findOne where a value from req.body or req.query sits directly in the filter. In a penetration test, AssistSec works through these type-dependent cases by hand, precisely because they slip past automated checks.
How do you prevent NoSQL injection?
- Validate every request against an explicit schema using a library such as zod, Joi or ajv, and reject unknown fields and wrong types before the application touches the data.
- Cast values explicitly to the type you expect. A
String()orNumber()immediately before the query makes it impossible for an object to end up in the filter. - Use an ODM with a strict schema, such as Mongoose, and avoid fields of type Mixed as well as filter objects you assemble by hand.
- Never compare passwords or tokens inside the database. Fetch the document by an identifying field, then compare the secret in the application against a hash.
- Disable server-side JavaScript if you do not need
$whereand mapReduce, and limit the database user to the privileges the application actually uses. - Never return raw database errors to the user. Log them internally; a stack trace tells an attacker exactly where their input lands.
- Have the API tested periodically. A focused penetration test and a code review catch the type-dependent cases that scanners skip.
Sources
Frequently asked questions
Is NoSQL injection less dangerous than SQL injection?
No. The shape of the attack differs, the consequences do not. A successful NoSQL injection still means a bypassed login, a dumped collection or a modified document. In some setups it reaches further, because certain NoSQL databases can execute JavaScript on the server.
Does Mongoose protect against NoSQL injection?
Largely yes, because a Mongoose schema casts values to the declared type and rejects what does not fit. That protection disappears the moment you build a filter yourself out of raw input, use a field of type Mixed, or turn strict mode off.
Which MongoDB operators do attackers use most?
In practice $ne and $gt to make a comparison always true, $regex to recover a value character by character, $in to try a list of candidates at once, and $where to get JavaScript running on the server.
Is stripping dollar signs from input enough?
Only partly. It blocks the most obvious payloads, but it is a blocklist and blocklists are never complete. It also mangles legitimate input. Type checking and schema validation solve the problem at the root instead.
Are JSON APIs more exposed than HTML forms?
Yes. A classic HTML form always produces text, while JSON preserves the difference between a string and an object and hands that difference straight to your code. GraphQL and REST endpoints that accept JSON are therefore the usual entry point.
Related articles
- GlossaryPentestA penetration test (pentest) is a controlled attack on your systems by ethical hackers. Learn how a pentest works and what vulnerabilities it uncovers.
- VulnerabilitiesCWE-287A07:2021Broken authenticationBroken authentication explained: how attackers take over accounts through brute force, leaked passwords and predictable session tokens, and how to stop them.
- VulnerabilitiesCWE-78A03:2021OS command injectionOS command injection explained: how shell metacharacters reach a system call, what an attacker gains, and why argument arrays without a shell fix it.
- VulnerabilitiesCWE-915A01:2021Mass assignmentMass assignment explained: how binding a whole request body onto a model makes isAdmin or balance writable, and how an allowlist prevents it.
- VulnerabilitiesCWE-89A03:2021SQL injectionSQL injection explained: how attackers use unfiltered input to read or change your database, what the impact is, and how prepared statements stop it.