NoSQL Injection: The Complete Practical Guide to MongoDB Injection

NoSQL Operator Injection and Authentication Bypass

How supplying a query operator where a string was expected bypasses MongoDB logins, with the $ne, $gt, $regex, and $in payloads, step by step.

Published

Operator injection is the most common and most useful NoSQL injection. It falls straight out of the type confusion from the databases intro: the application expected a string value and compared it for equality, but you send a query operator instead, and the comparison becomes something you control. In a login, that means authenticating with no valid password. This page walks the technique end to end. Try it on a free live target as you go.

Why login forms are the classic target

A login is the perfect victim because it does one thing: compare the submitted credentials against a stored document. The vulnerable code trusts that both fields are strings:

const user = await users.findOne({
  username: req.body.username,
  password: req.body.password,
});
if (user) grantSession(user);

If req.body is JSON, the client controls the type of every field. The developer pictured a string in password. You are going to put an operator there, and the equality check will never happen.

Step 1: find an input that reaches a query

Look for endpoints that accept JSON and use a field in a lookup: login, "forgot password," search, filters, anything that queries a collection by a value you supply. Send a normal request first and note the failure response, for example:

{ "username": "admin", "password": "wrongpass" }

You will get an "invalid credentials" style response. That baseline is your oracle for the next step.

Step 2: send an operator object

Replace the password string with an operator object that is always true. The workhorse is not-equal null:

{ "username": "admin", "password": { "$ne": null } }

The password clause now means "password is not equal to null," which matches any account whose password is not null, and even an account with no password field at all. In practice that is every real account. The equality check the developer intended is gone. If the username admin exists, you are logged in as admin without knowing the password.

Other always-true operators do the same job and are worth keeping handy, because a blocklist might catch one but not another:

{ "username": "admin", "password": { "$gt": "" } }
{ "username": "admin", "password": { "$ne": "this-is-not-the-password" } }

$gt with an empty string matches any non-empty string. The $ne with an arbitrary wrong value matches as long as the real password is not that exact value, which it never is.

Step 3: bypass without knowing any username

If you do not even know a valid username, make both fields always true:

{ "username": { "$ne": null }, "password": { "$ne": null } }

This matches the first document in the collection and logs you in as whoever that is. Because findOne returns the first match, that is usually the earliest-created account, which in a lot of applications is the administrator set up during installation.

Landing on a specific account

When the first match is not the user you want, steer the query. Use an anchored $regex to force the username, keeping the always-true password:

{ "username": { "$regex": "^admin" }, "password": { "$ne": null } }

Or try a shortlist of likely privileged names in one request with $in:

{ "username": { "$in": ["admin", "administrator", "root"] }, "password": { "$ne": null } }

This is the difference between "logged in as someone" and "logged in as the account I chose," and it is what turns a neat trick into a real account takeover.

Reading the result

The confirmation is simple: the response flips from your baseline failure to success. Depending on the app that is a redirect to a dashboard, a session cookie, a token in the body, or a different message. If the response is identical either way, you are in blind territory, and the same operators become a read primitive in blind and timing.

Where the payload can arrive

Everything above used a JSON body because it is the clearest case, but operator injection is not limited to JSON APIs. Many form and query-string parameters are parsed into nested objects by the server, so the exact same operators reach endpoints that never knowingly accept JSON. That delivery mechanism is important enough to get its own page: query-string injection.

Fixing it

Operator injection dies the instant the application refuses an operator-shaped value where it expected a string:

  • Validate the type of each field. If password is not a string, reject the request before it reaches the query.
  • Do not spread raw request bodies into a query. Pull named fields and cast them.
  • Use a strict schema so a field typed as a string will not accept a nested object.

Full per-driver code is in preventing NoSQL injection. Next, see how these operators ride in through ordinary parameters in query-string injection, or practice the auth bypass on a free target.

← Back to NoSQL Injection: The Complete Practical Guide to MongoDB Injection