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.
Search across all documentation pages
Map OWASP API Security Top 10 risks to concrete Node.js mitigations - Zod validation, object-level auth, rate limits, and dependency scanning.
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:
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:
tenantId and ownership on object access (404, not 403, to avoid enumeration).| 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 |
// 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);# Generate OpenAPI and diff on PR
npx @redocly/cli lint openapi.yamlapp.get, app.post in CI.req.user from optional middleware - open route if middleware skipped. Fix: explicit requireAuth per protected route.| 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 |
Yes in practice. Developers authenticate users but forget to check they own the :id in the path.
Yes for BOLA, auth, and SSRF. Rate limits can be looser but resource caps still matter.
API list is specific to REST/GraphQL backends. Pair with Security Headers & CORS for browser clients.
Yes. BOLA becomes field-level authorization; batch queries are API4 resource consumption.
Integration tests: user A token cannot read user B resource by ID. Automate in CI.
Scope keys per tenant and per operation. Rotate and audit usage. Never pass in query strings.
Partially - it is dependency risk. Add Helmet, config review, and penetration tests for full API8.
Use @UseGuards on controllers, not individual methods inconsistently. Global validation pipe with whitelist: true.
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.
Reviewed by Chris St. John·Last updated Jul 19, 2026