Skip to content

Security

mongoose-guard protects your API against four input attacks in request bodies: mass assignment (setting fields like role), prototype pollution (__proto__ keys), MongoDB operator injection ($where, $gt and friends) and type confusion (an object where a string is expected). It does not handle authentication, authorization, query-string injection, rate limiting, body size, or output escaping. Those still need their own protection.

The attack: sending fields the client shouldn’t control, such as role, isAdmin, credits or ownerId, hoping the server saves the whole body.

The defence: the allow list. Any schema field not listed for this route is a forbidden_field issue (or removed in strip mode). OWASP lists this under API3:2023, Broken Object Property Level Authorization.

The attack: a body like {"__proto__": {"isAdmin": true}}. If code later merges it into another object, every object in the process can inherit isAdmin: true.

The defence: __proto__, constructor and prototype are rejected as unsafe_key anywhere in the body. Real output:

{ "code": "unsafe_key", "path": "__proto__", "message": "Key \"__proto__\" is reserved" }

The attack: sending an object with operators where the server expects a plain value. The classic example is a login query built from { "email": { "$ne": null } }, which matches every user.

The defence, in two layers:

  1. Typed fields only accept their type. { "$ne": null } for a String field is invalid_type: Expected string, received object.
  2. Keys starting with $ are rejected anywhere they could slip through, including inside Mixed fields and Map keys. Real output for a Mixed field:
{ "code": "unsafe_key", "path": "preferences.filter.$where", "message": "Keys starting with $ are not allowed" }

Dotted keys like "address.city" are rejected too, because MongoDB interprets them as paths into nested documents.

The attack: sending a different type than the code expects (an array instead of a string, a string instead of a number) to trip up comparisons, string methods or queries.

The defence: strict type checks, with no casting. See Strict types.

In strip mode, unknown and forbidden fields are removed quietly. Unsafe keys are not: they always fail the request. An honest client has no reason to send $where or __proto__. Removing them silently would let a probing attacker go unnoticed in your logs, so mongoose-guard makes them visible.

ThreatWhy mongoose-guard doesn’t helpUse instead
Unauthenticated requestsIt checks content, not identityauth middleware before the guard
Editing someone else’s recordIt doesn’t know about req.params.id or ownershipan ownership check in the handler or a query filter
Injection through query strings and paramsIt only validates bodiesvalidate req.query and req.params with Zod, Joi or express-validator; cast IDs explicitly
Huge bodiesIt sees the body after parsingexpress.json({ limit: "100kb" })
Too many requestsNot its joba rate limiter
XSS through stored stringsIt doesn’t escape or sanitizeescape on output, or sanitize HTML with a dedicated library
Slow regular expressions in your schemaIt runs your match rules as writtenreview match patterns for catastrophic backtracking
Duplicate unique valuesunique isn’t a validator in Mongoosehandle E11000 errors on save

Please don’t open a public issue for a security problem. Use GitHub’s private vulnerability reporting on the repository.