A practical, code-first checklist for hardening REST APIs — written from production hardening of multiple Node.js + Express SaaS APIs. Companion to my OWASP SaaS Hardening Guide.
This is not a buzzword list. Every item is paired with concrete code, a test that would have caught the bug, and a one-line explanation of why it matters.
- Skim the checklist to spot gaps in your API.
- For each unchecked item, open the linked chapter for code + test.
- Tick boxes as you implement. Re-run the tests on every release.
- JWTs are stored in HttpOnly cookies, not localStorage. Why · Code
- JWT signing algorithm is locked. Reject
alg: none. Code - Refresh tokens rotate on every refresh, with reuse detection. Code
- Login endpoint is rate-limited per-IP and per-user. Code
- Password reset returns the same response regardless of email existence. Code
- MFA is supported and required for sensitive roles.
- Sessions can be revoked server-side on logout / password change.
- No token material in URL parameters, ever. They end up in logs.
- Every endpoint declares its required role explicitly. Default: deny. Code
- Tenant ID comes from the JWT, never from request body / path. Code
- Database-layer enforcement (RLS / equivalent) — bug at API doesn't leak data. Code
- 403 returns are uniform — don't leak resource existence vs. permission.
- Object-level access checks — IDOR test in CI: as User A, attempt every User B object.
- Every request body and query is validated against a schema (Zod, Joi, ajv). Code
- Schemas reject unknown fields by default. Don't trust
additionalProperties: true. - Numeric inputs have min/max bounds. Stop the
?limit=99999999query. - String inputs have max-length caps. Defeats memory-amplification.
- File-upload size and type checked server-side, not by the client.
- JSON serialization is the only output path — no template-string assembly of responses.
- Error responses sanitized in production. Stack traces logged, not returned. Code
- No internal IDs / pointers / secrets in error messages.
- Sensitive fields explicitly omitted from API responses (use
pick()patterns, notomit()).
- Per-IP rate limit on every endpoint, with
Retry-Afterheader. Code - Per-user (after auth) rate limit on expensive endpoints (search, export, AI calls).
- Stricter limits on auth, billing, password-reset endpoints.
- Alerting on 401/403 spikes — recon signal.
- No secrets in source.
gitleaks/trufflehogruns in CI. - App refuses to boot if required env vars are missing or weak. Code
- Secrets manager (Vault / KMS / Doppler / Supabase Vault) in production.
- Secrets rotated on a schedule + on suspicion.
- Different secrets per environment. Production secret never seen by a developer locally.
- HSTS preload-eligible (
max-age=31536000; includeSubDomains; preload). - Strict CSP, no
unsafe-inline/unsafe-eval. - X-Content-Type-Options: nosniff.
- X-Frame-Options: DENY (or CSP
frame-ancestors 'none'). - Referrer-Policy: strict-origin-when-cross-origin.
- Permissions-Policy: minimal allow-list.
- TLS 1.2+ only. Prefer 1.3.
- HTTPS enforced everywhere, including dev environments.
- Modern cipher suites only (no RC4, no 3DES).
- Edge proxy in front of API origin.
- mTLS between internal services where applicable.
- Every privileged action writes to an append-only audit log. Code
- No PII / tokens / passwords in logs. Redact at the boundary. Code
- Structured logs (JSON), shipped to a queryable store.
- Alert on
auth.refresh_reuse_detectedevents. - Alert on Zod parse failure spikes — cheap injection-attempt detector.
- Webhook signature failures alerted.
- Signature verified on every event before any state change. Code
- Idempotent processing. Replay → no-op. Code
- Webhook handler runs in a transaction with the resulting state change.
- Edge rate-limit on webhook endpoints so unsigned floods are rejected fast.
- Don't accept user-supplied URLs unless absolutely required.
- DNS resolved server-side, IP validated against public-only allow-list. Code
- HTTPS-only. No
gopher:,file:,dict:. - Redirects disabled on outbound fetch.
- AWS IMDSv2 enforced if running on EC2.
- Dependabot / Renovate enabled.
-
npm audit --audit-level=high(or equivalent) gates CI. - SBOM generated on every release.
- Subresource Integrity for any CDN-loaded scripts.
- OpenAPI spec is the source of truth for the API contract.
- Auth requirements documented per endpoint.
- Rate limits documented.
-
SECURITY.mdexists with a vulnerability disclosure policy.
| Topic | Chapter |
|---|---|
| Authentication | chapters/authentication.md |
| Authorization | chapters/authorization.md |
| Input validation | chapters/input-validation.md |
| Output & errors | chapters/output.md |
| Rate limiting | chapters/rate-limiting.md |
| Configuration | chapters/config.md |
| Logging | chapters/logging.md |
| Webhooks | chapters/webhooks.md |
| SSRF | chapters/ssrf.md |
Status: chapters are seeded with summary content and links into the OWASP SaaS Hardening Guide. They expand over time (see ROADMAP.md).
- 📚 owasp-saas-hardening-guide — OWASP Top 10 with code.
- 🎯 threat-modeling-framework — STRIDE templates.
- 🔍 security-audit-toolkit — Cloud config auditing.
MIT