SQL injection is the vulnerability everyone learned about, and "we use MongoDB, so we don't have SQL injection" is a sentence we still hear in discovery calls. It is true and irrelevant. MongoDB-backed applications are vulnerable to NoSQL injection, the MEAN stack's JSON-all-the-way-down design makes it unusually easy to trigger, and in our 2026 audits it remains the most common critical finding. Here is what it looks like, why Express applications are exposed, and the checklist we apply.
Anatomy of an operator injection
Consider a login handler that most tutorials still teach:
app.post('/api/login', async (req, res) => {
const user = await User.findOne({
email: req.body.email,
password: req.body.password, // pretend this is hashed; the problem is the same
});
if (!user) return res.status(401).end();
res.json({ token: issueToken(user) });
});
A normal client sends {"email":"a@b.com","password":"hunter2"}. An attacker sends:
{ "email": "a@b.com", "password": { "$ne": "" } }
express.json() parses that into an object, the object flows straight into the query, and the query becomes "find a user with this email whose password is not empty." The first match is returned. No credentials required. Variations use $gt, $regex (to extract secrets one character at a time), or $in with a list of guesses. With Express 4's extended query parser, the same attack works through the URL: ?password[$ne]= becomes a nested object in req.query.
$where and server-side JavaScript
The second class is older but still present in legacy MEAN.JS codebases:
db.collection('orders').find({ $where: `this.customerId == '${req.query.customer}'` });
$where evaluates JavaScript on the server. String interpolation into it is textbook injection, including the ability to run denial-of-service payloads. Modern MongoDB restricts what $where can do, but the right answer is to not use it at all; an $expr with aggregation operators covers the legitimate cases. MongoDB's own $where documentation says the same.
Why MEAN apps are especially exposed
- The request body is already an object, so there is no string boundary where a developer instinctively escapes things.
- Mongoose casts values but does not reject operators in plain
findOne(filter)calls when the filter is built from user input. - Express 4's default
qs-based query parser turns bracket syntax into nested objects, extending the attack toGETrequests. - A generation of tutorials passed
req.bodydirectly intoModel.find().
Defenses that work
1. Validate the shape of input before it touches a query. A schema validator (Zod, Joi, or JSON Schema via Ajv) that declares password must be a string rejects the { "$ne": "" } payload before any database code runs. This is the primary control; everything else is defense in depth.
import { z } from 'zod';
const LoginBody = z.object({
email: z.string().email(),
password: z.string().min(8).max(128),
});
app.post('/api/login', async (req, res) => {
const body = LoginBody.parse(req.body); // throws a 400-able error on operator objects
const user = await User.findOne({ email: body.email });
if (!user || !(await argon2.verify(user.passwordHash, body.password))) {
return res.status(401).end();
}
res.json({ token: issueToken(user) });
});
2. Sanitize as a backstop. express-mongo-sanitize strips or escapes keys beginning with $ or containing . from req.body, req.query, and req.params. Note that in Express 5, req.query is a read-only getter, so configure the middleware to sanitize in place or apply it to req.body and handle query validation with your schema layer.
3. Keep the simple query parser. Express 5 defaults req.query to the simple parser, which does not build nested objects. Do not set query parser to extended unless you have a validated reason.
4. Enable Mongoose's strict query mode and enforce types. Mongoose 8 defaults strictQuery to false; set it to true so filters on undefined paths are stripped, and define every queried field with an explicit type so casting fails on objects.
5. Never build $where, mapReduce, or $function payloads from user input. Disable server-side JavaScript entirely with security.javascriptEnabled: false in mongod.conf if nothing legitimate uses it. Atlas clusters have it disabled by default.
6. Least-privilege database users. The application's MongoDB user should have read/write on its own database and nothing else; not root, not dbAdmin, and certainly not the user that runs migrations.
JWT pitfalls that travel with injection
An attacker who bypasses login still needs a token; make sure that is not the easy part.
- Pin the algorithm when verifying:
jwt.verify(token, key, { algorithms: ['RS256'] }). Never accept whateveralgthe token header claims. - Use asymmetric keys (RS256 or EdDSA) so the API server holds only a public key.
- Short-lived access tokens (minutes), rotating refresh tokens stored server-side, and a revocation path.
- Do not put authorization data the client can edit into a token you then trust without re-checking.
Supply-chain hygiene
Injection is only one entry point; most of the 2025 to 2026 incidents we have responded to started with a compromised npm package.
- Run
npm auditin CI and fail the build on high or critical findings; use Snyk or GitHub Dependabot for continuous monitoring. - Commit
package-lock.jsonand install withnpm ci. - Enable provenance verification where your registry supports it, and review new transitive dependencies on major version bumps.
- Keep Node.js on an LTS line that still receives security releases (24 or 22 as of August 2026).
The 10-point MEAN security checklist
- Every request body and query string validated against a schema before use.
express-mongo-sanitize(or equivalent) as a backstop.- Simple query parser in Express 5; no
extendedwithout justification. - No
$where,$function, ormapReducewith user-influenced input; server-side JS disabled. - Mongoose
strictQuery: trueand typed schemas on every model. - Password hashing with Argon2id or bcrypt at a current work factor.
- JWT verification with a pinned algorithm and short-lived access tokens.
- Rate limiting on authentication and other sensitive endpoints.
helmetfor HTTP headers and strict CORS origins.npm auditin CI, locked dependencies, Node.js on a supported LTS.
We run this checklist as part of every MEAN stack testing engagement and every application we inherit for rescue. If you would like an independent review of yours, contact us.