Here is an example of Node.js code that is vulnerable to NoSQL injection:
// Vulnerable: the request body is passed straight into the query filter
app.post("/login", async (req, res) => {
const user = await db.collection("users").findOne({
username: req.body.username,
password: req.body.password
});
if (!user) {
return res.status(401).send("Invalid credentials");
}
req.session.userId = user._id;
res.send("Logged in");
});A JSON body such as {"username":"admin","password":{"$ne":null}} turns the equality check into an operator query. MongoDB happily returns the admin document and the attacker is logged in without ever knowing the password. Because the values are objects rather than strings, operators like $ne, $gt, $regex, and $where can be smuggled into the filter from any client.
Here is a version of the same code that is secured against NoSQL injection:
// Secure: force scalars, then verify the password hash in the application
app.post("/login", async (req, res) => {
const { username, password } = req.body;
if (typeof username !== "string" || typeof password !== "string") {
return res.status(400).send("Invalid input");
}
const user = await db.collection("users").findOne({ username: { $eq: username } });
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).send("Invalid credentials");
}
req.session.userId = user._id;
res.send("Logged in");
});The type checks reject objects and arrays before they reach the database, $eq pins the comparison to a literal value, and the password never becomes part of the query at all - it is compared against a bcrypt hash after the document is loaded. Adding schema validation at the edge with Zod, Joi, or express-mongo-sanitize removes the whole class of operator injection.