OWASP Top 10 for APIs
Map OWASP API Security Top 10 risks to concrete Node.js mitigations - Zod validation, object-level auth, rate limits, and dependency scanning.
Recipe
Quick-reference recipe card - copy-paste ready.
// Pattern: validate -> authenticate -> authorize -> handler
app.get("/orders/:id", async (req, res) => {
const user = requireAuth(req);
const orderId = z.string().uuid().parse(req.params.id);
const order = await ordersRepo.findById(orderId);
if (!order || order.tenantId !== user.tenantId) {
return res.status(404).json({ error: "Not found" });
}
res.json(order);
});When to reach for this:
- Security review before public API launch.
- SOC2 or penetration test remediation.
- Onboarding checklist for new HTTP services.
- Quarterly audit of internal B2B APIs.
Working Example
import express from "express";
import rateLimit from "express-rate-limit";
import { z } from "zod";
import helmet from "helmet";
class HttpError extends Error {
constructor(public status: number, message: string) {
super(message);
}
}
const usersRepo = {
async update(id: string, patch: { name: string }) {
return { id, ...patch };
},
};
const app = express();
app.use(helmet());
app.use(express.json({ limit: "1mb" }));
const authLimiter = rateLimit({ windowMs: 60_000, max: 10 });
app.post("/auth/login", authLimiter, async (req, res) => {
const body = z.object({
email: z.string().email(),
password: z.string().min(8),
}).safeParse(req.body);
if (!body.success) return res.status(400).json({ error: "Invalid input" });
// constant-time compare in real auth service
res.json({ token: "..." });
});
type User = { id: string; tenantId: string; role: string };
function requireAuth(req: express.Request): User {
const user = (req as express.Request & { user?: User }).user;
if (!user) throw new HttpError(401, "Unauthorized");
return user;
}
app.patch("/users/:id", async (req, res) => {
const actor = requireAuth(req);
const targetId = z.string().uuid().parse(req.params.id);
if (actor.id !== targetId && actor.role !== "admin") {
return res.status(403).json({ error: "Forbidden" });
}
const patch = z.object({ name: z.string().min(1).max(120) }).parse(req.body);
res.json(await usersRepo.update(targetId, patch));
});What this demonstrates:
- API2/API5: rate limit sensitive auth endpoints.
- API1: verify
tenantIdand ownership on object access (404, not 403, to avoid enumeration). - API3: mass assignment blocked by explicit Zod patch schema.
- API8: bounded JSON body size.
Deep Dive
OWASP API Top 10 Mitigations (Node.js)
| Risk | Node mitigation |
|---|---|
| API1: BOLA | Check resource.ownerId === user.id (or tenant) on every ID route |
| API2: Broken Auth | bcrypt/argon2, short-lived JWT, rate limit /login, rotate secrets |
| API3: Mass Assignment | Zod parse with explicit allowed fields; never spread req.body into ORM |
| API4: Resource Consumption | Body limits, pagination caps, rate limits, queue heavy jobs |
| API5: BFLA | Role checks per endpoint; admin routes behind separate middleware |
| API6: Unrestricted Flow | OTP rate limits, captcha on signup, idempotency keys on payments |
| API7: SSRF | URL allowlist, DNS resolve + block private IPs (SSRF Guards) |
| API8: Misconfiguration | Helmet, disable x-powered-by, no default creds, npm audit in CI |
| API9: Inventory | OpenAPI spec, route lint, deprecate unused endpoints |
| API10: Unsafe Consumption | Validate third-party webhook payloads; timeout outbound fetches |
Express vs Fastify
// Fastify: JSON Schema at route + preHandler auth
app.get("/orders/:id", {
schema: { params: { type: "object", properties: { id: { type: "string", format: "uuid" } } } },
preHandler: [authenticate, authorizeOrderRead],
}, handler);- Fastify compiles validators at startup - fails invalid routes early.
- NestJS: guards for authz, pipes for validation - same rules apply.
Inventory Discipline
# Generate OpenAPI and diff on PR
npx @redocly/cli lint openapi.yaml- Undocumented routes are API9 debt - grep
app.get,app.postin CI.
Gotchas
- 404 vs 403 on BOLA - 403 confirms resource exists. Fix: return 404 for cross-tenant access.
- JWT in localStorage - XSS steals tokens. Fix: httpOnly cookies for browser clients.
- Admin flag in JWT without server check - tampered claims. Fix: load roles from DB each request or short TTL + refresh.
- Webhook endpoints without signature verify - forged events. Fix: HMAC verify (Stripe, GitHub patterns).
- GraphQL introspection in production - API9 exposure. Fix: disable or auth-gate in prod.
- Trusting
req.userfrom optional middleware - open route if middleware skipped. Fix: explicitrequireAuthper protected route.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Manual Zod + middleware | Full control, small services | Large NestJS app with built-in pipes |
| NestJS Guards + Pipes | Decorator-driven authz | Non-Nest greenfield |
| Fastify JSON Schema | Performance-critical validation | Team prefers Zod only |
| API gateway auth | Centralized edge policy | Still need BOLA checks in app |
FAQs
Is API1 really the top risk?
Yes in practice. Developers authenticate users but forget to check they own the :id in the path.
Do I need all OWASP controls for internal APIs?
Yes for BOLA, auth, and SSRF. Rate limits can be looser but resource caps still matter.
How does this relate to OWASP Top 10 web?
API list is specific to REST/GraphQL backends. Pair with Security Headers & CORS for browser clients.
Should GraphQL use the same list?
Yes. BOLA becomes field-level authorization; batch queries are API4 resource consumption.
How to test BOLA?
Integration tests: user A token cannot read user B resource by ID. Automate in CI.
What about API keys?
Scope keys per tenant and per operation. Rotate and audit usage. Never pass in query strings.
Does npm audit cover API8?
Partially - it is dependency risk. Add Helmet, config review, and penetration tests for full API8.
NestJS specific tip?
Use @UseGuards on controllers, not individual methods inconsistently. Global validation pipe with whitelist: true.
Related
- Security Basics - foundational examples
- SSRF Guards - API7
- Prototype Pollution - unsafe merges
- Security Rules - team baseline rules
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.