Chapter 48 — API Security
📖 Goal
Make APIs robust against the most common attack classes (OWASP Top 10) without sacrificing developer velocity.
🛡️ Defense-in-Depth Checklist
| Concern | Defense |
|---|---|
| Sniffing | HTTPS everywhere (HSTS) |
| Injection (SQL/NoSQL) | Parameterized queries, ODM/ORM, validation |
| XSS | Escape output, CSP, sanitize HTML |
| CSRF | SameSite cookies + CSRF tokens for non-idempotent requests |
| Mass assignment | Whitelist DTO fields |
| Broken auth | Strong password hashing, MFA, account lockout |
| Sensitive data exposure | TLS, encryption at rest, don't log PII |
| Broken access control | Centralized authorization checks |
| Rate / DoS | Rate limiting, request size limits, circuit breakers |
| Logging gaps | Structured logs without secrets, alerts |
| Misconfiguration | Helmet, CORS allowlist, no default creds |
| Insecure deserialization | Validate JSON shape; avoid eval/Function on input |
| Vulnerable deps | npm audit, Snyk, automated updates |
| Insufficient logging | Audit log for sensitive actions |
💻 Code Example — Express Security Baseline
import express from "express";
import helmet from "helmet";
import cors from "cors";
import rateLimit from "express-rate-limit";
const app = express();
// HTTP headers
app.use(helmet());
// JSON body cap
app.use(express.json({ limit: "1mb" }));
// CORS allowlist
app.use(cors({
origin: ["https://app.example.com"],
credentials: true,
methods: ["GET", "POST", "PATCH", "DELETE"],
}));
// Global rate limit
app.use(rateLimit({ windowMs: 60_000, max: 100 }));
// Auth-specific limit
app.use("/api/auth", rateLimit({ windowMs: 15*60*1000, max: 5 }));
app.use("/api", apiRoutes);💻 Code Example — NoSQL Injection (and Defense)
// ❌ Vulnerable — body is { email: { $ne: null } }
User.findOne({ email: req.body.email });
// ✅ Coerce to string first
User.findOne({ email: String(req.body.email) });
// ✅ Or validate with Zod / DTO
const schema = z.object({ email: z.string().email(), password: z.string() });
const parsed = schema.parse(req.body);
User.findOne({ email: parsed.email });💻 Code Example — Mass Assignment Protection
// ❌ Caller could set role: "admin"
User.create(req.body);
// ✅ Whitelist fields
const { email, password, name } = req.body;
User.create({ email, password, name });
// or in Nest:
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));💻 Code Example — Password Hashing
import bcrypt from "bcrypt";
const hash = await bcrypt.hash(password, 12); // cost factor 12
const ok = await bcrypt.compare(input, user.password);For higher security needs, use argon2id:
import argon2 from "argon2";
const hash = await argon2.hash(password, { type: argon2.argon2id });💻 Code Example — CSRF Protection (Cookie-Based Auth)
import csrf from "csurf";
app.use(csrf({ cookie: true }));
app.get("/csrf-token", (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// Frontend includes the token in X-CSRF-Token header for POST/PUT/DELETEFor JWT-in-Authorization-header auth, CSRF is less of a concern because cookies aren't automatically sent with Authorization requests.
💻 Code Example — Security Headers (manual)
res.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload");
res.set("X-Content-Type-Options", "nosniff");
res.set("X-Frame-Options", "DENY");
res.set("Referrer-Policy", "strict-origin-when-cross-origin");
res.set("Permissions-Policy", "geolocation=(), camera=()");
res.set(
"Content-Security-Policy",
"default-src 'self'; img-src 'self' data:; script-src 'self'"
);helmet() adds all of these by default.
💻 Code Example — Cookie Hardening
res.cookie("refresh", token, {
httpOnly: true, // not accessible via JS — XSS-safe
secure: true, // HTTPS only
sameSite: "strict", // CSRF protection
path: "/api/auth", // limit which paths send it
maxAge: 7 * 24 * 60 * 60 * 1000,
});💻 Code Example — Input Limits
// Body size
app.use(express.json({ limit: "100kb" }));
// Param length
app.param("id", (req, res, next, val) => {
if (val.length > 64) return res.status(400).end();
next();
});
// File upload size — done by Multer (see Chapter 29)💻 Code Example — Logging Without Secrets
import pino from "pino";
const logger = pino({
redact: {
paths: ["req.headers.authorization", "req.body.password", "req.body.token"],
censor: "[redacted]",
},
});💻 Code Example — Audit Log
async function audit(req, action, target) {
await AuditLog.create({
actorId: req.user.id,
action, // e.g., "DELETE_USER"
target, // e.g., "user:abc123"
ip: req.ip,
userAgent: req.get("user-agent"),
at: new Date(),
});
}
router.delete("/users/:id", requireRole("admin"), async (req, res) => {
await UserService.remove(req.params.id);
await audit(req, "DELETE_USER", `user:${req.params.id}`);
res.status(204).end();
});💻 Code Example — Dependency Auditing
npm audit
npm audit fix --force # only after reviewing breaking changes
# CI integration
npx audit-ci --moderate
# Snyk
npx snyk test💻 Code Example — Secrets Management
// ❌ Never
const apiKey = "sk_live_abc123";
// ✅ Environment variable
const apiKey = process.env.STRIPE_API_KEY;
// ✅ Or a secrets manager (AWS Secrets Manager, HashiCorp Vault)
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
const sm = new SecretsManagerClient();
const { SecretString } = await sm.send(new GetSecretValueCommand({ SecretId: "stripe" }));
const { apiKey } = JSON.parse(SecretString);Never commit .env. Use .env.example for templates.
💻 Code Example — Login Brute-Force Protection
- Rate-limit
/loginaggressively (5 attempts / 15 min per IP + per email). - Add account lockout after N failed attempts (with a cooldown).
- Use CAPTCHA after a few failures.
- Send "suspicious activity" emails on new device logins.
🎯 Likely Interview Questions
- How do you secure a Node.js API?
- What's CORS, and how do you configure it? — Browser security model that restricts cross-origin XHR. Set
Access-Control-Allow-Originto an allowlist (never*with credentials). - How do you prevent SQL/NoSQL injection?
- Where do you store secrets?
- What's the difference between authentication and authorization?
- What's CSRF and how do you prevent it?
← Monolith vs Microservices | Index | Next: Self-Introduction →