Web security basics for APIs
Injection, mass assignment, CORS, rate limits and the leaks tests look for.
Injection
- NoSQL:
{ email: req.body.email }where email is{ "$ne": null }matches everyone. Checktypeof === "string". - SQL: always use placeholders:
db.query("SELECT * FROM users WHERE id = $1", [id]), never string concatenation. - Regex: escape user text before
new RegExp(q).
Mass assignment
await User.updateOne({ _id: id }, req.body); // ❌ client sends { role: "admin" }
const { name, bio } = req.body;
await User.updateOne({ _id: id }, { name, bio }); // ✅ allow-list
Broken access control
The most common real-world bug: checking that a user is logged in, but not that they own the record. Every /:id route that returns private data needs an ownership or role check.
Leaks
- Password hashes, tokens, internal ids in responses.
- Stack traces in 500 responses.
- Different messages for "unknown email" and "wrong password" (lets attackers enumerate accounts).
CORS
CORS is enforced by browsers, not by your server. cors() with no options allows every origin; fine for a public API, wrong for cookies. With credentials, set an explicit origin list and credentials: true. CORS does not protect against curl or server-to-server calls.
Rate limiting
Login, signup and password reset need limits per IP and per account. Return 429 with Retry-After. Key by req.ip only after configuring trust proxy correctly, or every user behind the load balancer shares one bucket.
Headers
helmet() sets sensible defaults (no X-Powered-By, content-type sniffing off, and so on). Cheap, and reviewers notice.