Injection

Injection and Query Sanitization

MongoDB 1,815 has no string query language to concatenate, so classic SQL injection has no equivalent. What replaces it is operator injection: a query value arrives as an object instead of a string, and { email: { $ne: null } } matches every user. Express 5 24,430 closed the common route to that almost by accident, switching the default query parser from extended to simple so bracket syntax no longer becomes nested objects.

The same request under both query parsersJavaScript
app.get('/q', (req, res) => res.json({ query: req.query, emailType: typeof req.query.email }));
// app.set('query parser', 'extended');   // opt back in to qs and nested objects
Output
GET /q?email[$ne]=null&__proto__[polluted]=yes
simple    {"query":{"email[$ne]":"null","__proto__[polluted]":"yes"},"emailType":"undefined"}
extended  {"query":{"email":{"$ne":"null"}},"emailType":"object"}

Under simple the bracket text is part of the key, so an operator can never appear; under extended it becomes a live object. Object.prototype.polluted stayed undefined either way, because qs refuses to write prototype keys. A JSON body is always parsed by JSON.parse, though, so bodies hold objects under any parser. The real fix is validation: z.string() rejects an object, z.strictObject() rejects unexpected fields (Schema Validation with Zod).

Rejecting an injected operator and an escalated roleJavaScript
const newUser = z.strictObject({ email: z.string().email(), password: z.string().min(12) });
app.use(express.json({ limit: '10kb' }));               // cap the body before parsing it
app.post('/safe/users', (req, res) => {
  const p = newUser.safeParse(req.body);
  if (!p.success)
    return res.status(400).json({ error: 'invalid_body', issues: p.error.issues });
  res.status(201).json({ saved: { id: 'u_77', email: p.data.email, role: 'reader' } });
});
Output
POST /naive/users  {"email":"mallory@example.com","password":"...","role":"admin"}
  201 {"saved":{"id":"u_77","email":"mallory@example.com","password":"...","role":"admin"}}
POST /safe/users   (same body)
  400 {"error":"invalid_body","issues":[{"code":"unrecognized_keys",
       "message":"Unrecognized key: \"role\""}]}
POST /safe/users   {"email":"a@example.com","password":"<20 kB of x>"}
  413 text/html  PayloadTooLargeError: request entity too large

The naive route is a mass assignment bug: { ...req.body } handed the client a promotion to admin. A non-strict schema would have stripped role silently, which hides the attempt; strictObject turns it into a 400 you can alert on. Never spread a request body into a persisted object - list the fields you accept. The 413, meanwhile, returned text/html with a stack trace: a centralized error handler (Error Middleware) is part of hardening too.

Two surfaces stay yours: a route fetching a client-supplied URL is server-side request forgery, so check its host against an allowlist, and a path built from req.params must be resolved (Untrusted Input).