NoSQL injection example: a CTF walkthrough

10 min read

TL;DR: NoSQL injection is when an application takes JSON from a user and uses it in a database query without checking its type, so a value the app expected to be a string can arrive as a query-operator object instead. On MongoDB, sending { "$ne": "x" } where an id string was expected turns "update this one review" into "update every review whose id is not x" — all of them. You prevent it by casting inputs to the type you expect and validating the request body against a schema, so an object can never land where a scalar belongs.

This is for learners and CTF players who know SQL injection and want to see its MongoDB cousin. We work the NoSQL manipulation challenge in CyberGym, our deliberately vulnerable training app, read the review endpoint to see the MongoDB store for ourselves, and fix the flaw. Every request targets CyberGym; only test apps you own or have written permission to test.

Watch on YouTube: HackerGPT vs NoSQL Injection Challenge

The video solves the challenge twice — by hand in Burp Suite, replaying a captured review request, and with HackerGPT, which maps the endpoints, authenticates, and crafts the operator payload itself. This article restructures it around the one idea that makes NoSQL injection different from SQL injection: the attacker injects a query operator, not query syntax.

What is NoSQL injection?

NoSQL injection is user-controlled input changing a database query on a document store such as MongoDB. OWASP documents it in the Web Security Testing Guide under WSTG v4.2 section 4.7.5.6, Testing for NoSQL Injection, and MITRE's nearest weakness is CWE-943, Improper Neutralization of Special Elements in Data Query Logic. It sits in the same A05, Injection, family as SQL injection in the OWASP Top 10:2025.

The twist is where the injection happens. SQL injection breaks out of a string by closing a quote and writing new SQL text. MongoDB does not take a string of query text — it takes a structured object, often built straight from JSON the client sent. So the attacker does not inject syntax; they inject an operator. MongoDB query operators begin with $ — $ne (not equal), $gt (greater than), $regex — and if an application copies user JSON into a query without checking that a field is a plain value, a user can send one of those operator objects where the app expected a string.

How can changing a value into an object break a query?

Because MongoDB treats { "id": "abc" } and { "id": { "$ne": "x" } } as two different kinds of condition. The first matches the one document whose id is "abc". The second matches every document whose id is not "x" — which, since no real id is the string "x", is all of them. The diagram makes the difference concrete.

A review id as a string matches one record; as a MongoDB not-equal operator object it matches every review A scalar id matches one record. An operator object the app never flattened to a string matches them all.

That is the whole CyberGym challenge: the review-update endpoint expects the id of the one review you are editing, but if it passes your JSON into the MongoDB query without flattening it to a string, you can replace the id with { "$ne": "x" } and your new text is written to every review in the collection at once. In the Juice Shop code CyberGym runs, the update handler matches { _id: req.body.id } and passes multi: true, so every document the condition matches is updated, not only the first one. The challenge counts as solved when more than one review is modified.

Reading the target: is this really MongoDB?

Before injecting anything, confirm the store is a document database — otherwise the operator trick does not apply. You can see it from a plain read of the product reviews. Here is a real request against CyberGym on 2 November 2026, for the reviews on product 42 (the "Best CyberGym Salesman Artwork" the video uses):

$ curl -s https://cybergym.hackergpt.app/rest/products/42/reviews
{"status":"success","data":[
  {"message":"I'd stand on my head to make you a deal for this piece of art.",
   "author":"stan@juice-sh.op","product":42,"likesCount":0,"likedBy":[],
   "_id":"udxLEzNRKwBWsvqp6","liked":true},
  {"message":"Just when my opinion of humans couldn't get any lower, along comes Stan...",
   "author":"bender@juice-sh.op","product":42,"likesCount":0,"likedBy":[],
   "_id":"EfnZBa8i8xNzkzDTc","liked":true}]}

The tell is the _id field: a random string like udxLEzNRKwBWsvqp6, not the integer primary key you would see from a relational table. Reviews in CyberGym live in a document collection, separate from the SQLite store that holds products and users: the Juice Shop source keeps them in MarsDB, an in-memory database with a MongoDB-style query API, so MongoDB operators apply. That mix is itself a useful observation — the same application can be injectable in two completely different ways depending on which store an endpoint talks to, which is why mapping the attack surface endpoint by endpoint matters more than knowing one payload.

How the review-update challenge is solved

The manual route in the video is the pentester's loop: submit a normal review, capture the resulting request in Burp's proxy history, and read it next to the GET that lists a product's reviews. That listing gives each review's _id, the value the server matches on, and the request carries the message, the new text. Sending the request to Burp's Repeater lets you edit and resend it without clicking through the UI each time.

The request the tester captures and edits in the video is PUT /rest/products/<id>/reviews — the endpoint that creates a review. They add "_id": { "$ne": -1 } to its body and get 201 Created, and a new "Manual NoSQL Pwned" review appears beside the old ones. That is a useful dead end to learn from: the create handler inserts a fresh review and ignores any _id in the body, so the operator never reaches a query. The challenge was also already marked solved on the scoreboard before the manual attempt began. The update lives on a different route, PATCH /rest/products/reviews, and that is the one HackerGPT finds when it works the same challenge in CTF Mode. It reads the front-end JavaScript bundle to find the PATCH endpoint, logs in with a test account whose credentials it finds in that bundle to get a JWT, and sends this body with no product in the URL:

{ "id": { "$ne": "nonexistent" }, "message": "NoSQL Injection successful!" }

The server replies with "modified": 33, which means thirty-three reviews were rewritten in one request, across every product that had reviews. The operator is the same as in the manual attempt. What changed is the route, the method and the field: id, which the update handler matches against _id. An operator only does anything when it lands in a query that selects which records to change.

The lesson is not the payload; it is the recognition step. HackerGPT noted that the app "may be directly passing user input into a MongoDB query" and tested that hypothesis with the smallest possible change — a value turned into an operator. That is the same hypothesis-first habit the CTF walkthrough docs record for this target's other challenges, and the one CTF for beginners teaches from the start.

How do I prevent NoSQL injection?

Make sure a value stays a value. The root cause is that an object arrived where the application expected a string, so the defences all enforce the expected type before the input reaches the query. OWASP's testing guide and cheat-sheet guidance come down to a few concrete steps:

  • Cast inputs to their expected type. If an id must be a string, coerce it to a string (or reject the request when it is not one) before building the query. A string can never carry a $ne.
  • Validate the request body against a schema. Use a schema validator (JSON Schema, Zod, Joi, Mongoose schema types) so a field declared as a string is refused when an object is sent. This is the single most reliable control, because it stops the shape of the input, not a specific payload.
  • Reject keys that start with $ (and dots) in user input, so query operators cannot be smuggled through, unless an endpoint genuinely needs them.
  • Do not build queries from raw user JSON. Read the specific fields you need and construct the query object yourself, rather than spreading the request body into the filter.

To check your own MongoDB-backed app, find an endpoint that looks up or updates by an id or a field value, and send that field as { "$ne": "x" } (or { "$gt": "" }) instead of the normal value — on a target you own or are permitted to test. If the response changes in a way a single record could not explain, the input is reaching the query untyped. The short definition is in our glossary entry on SQL injection for the relational sibling, and the same "check your own app" approach for a whole stack is in how to pentest your own website.

Key takeaways

  • NoSQL injection injects a query operator, not query syntax — the break-out is a type confusion, not a broken quote.
  • On MongoDB, { "$ne": "x" } where a value was expected matches every document, turning one update into all of them.
  • You can confirm a document store from a plain read: random string _ids, not integer keys, point to MongoDB.
  • The fix is type enforcement: cast inputs to the expected type and validate the body against a schema, so an object can never land where a scalar belongs.
  • Reject $-prefixed keys in user input and build query objects from specific fields rather than from raw request JSON.

FAQ

What is NoSQL injection in simple terms?

It is when an app uses JSON from a user in a database query without checking the type of each field, so a value the app expected can arrive as a query-operator object instead. On MongoDB, that lets an attacker send something like { "$ne": "x" } where a plain id was expected, changing which documents the query matches. It is the document-database equivalent of SQL injection.

How is NoSQL injection different from SQL injection?

SQL injection breaks out of a string in a text query by closing a quote and writing new SQL. NoSQL injection does not need quotes or syntax: MongoDB queries are structured objects, so the attacker injects an operator such as $ne or $gt by sending an object where a value was expected. The defences differ too — parameterization is the SQL answer, while type and schema validation is the NoSQL answer.

What does the $ne operator do in a NoSQL injection?

$ne means "not equal". Used as { "$ne": "x" } in a field the app looks up by, it matches every document whose value is not the string "x" — effectively all of them, since no real record has that value. That is how one update or one query can be made to affect an entire collection instead of a single record.

How do I prevent NoSQL injection in a MongoDB app?

Enforce the expected type of every input before it reaches a query: cast an id to a string, or reject the request if it is not one. Validate the whole request body against a schema so a field declared as a string is refused when an object is sent, and reject keys beginning with $ in user-supplied data. Build the query object from the specific fields you need rather than from the raw request JSON.

Is NoSQL injection still a real risk?

Yes. Document stores are widely used, and the mistake — passing user JSON straight into a query — is easy to make in any framework that reads the request body as an object. OWASP tracks NoSQL injection in its Web Security Testing Guide, and it falls under the Injection category of the OWASP Top 10:2025. Practise it only on apps you own or are permitted to test, such as CyberGym.

Sources

Try the $ne check on your own app with HackerGPT, or practise NoSQL injection on CyberGym.

  • SQL injection example: a CTF walkthrough

    How SQL injection works, shown on the product search of CyberGym, a deliberately vulnerable training app: confirm the injection point from a real database error, see how UNION enumeration and the OR 1=1 login bypass work, and fix it with parameterized queries.

  • XSS examples: DOM, reflected and stored

    The three kinds of cross-site scripting, each shown on a real challenge in CyberGym, a deliberately vulnerable training app, with a harmless reflection you can reproduce yourself and the output encoding that fixes all three.

  • IDOR vulnerability example: find it and fix it

    What an IDOR is, shown on the real basket handler of CyberGym, a deliberately vulnerable training app, then reproduced and fixed in a small local Express app with two users.

All articles on Web vulnerabilities