Step 5
Rate limit + CORS + security headers
25 min
Rate limit + CORS + security headers
Three surfaces that block most automated attacks when done cleanly.
1. Sliding window rate limit (Redis)
async function rateLimit(key: string, max: number, windowSec: number) {
const bucket = Math.floor(Date.now() / 1000 / windowSec);
const k = `rl:${key}:${bucket}`;
const count = await redis.incr(k);
if (count === 1) await redis.expire(k, windowSec * 2);
return { allowed: count <= max, count, max };
}
const { allowed } = await rateLimit(`login:${ip}`, 5, 60);
if (!allowed) return NextResponse.json({ error: "rate_limit" }, { status: 429 });
2. Without Redis
SELECT count(*) FROM rate_limit_events
WHERE key = $1 AND created_at > now() - interval '1 minute';
3. Per-route budgets
| Route | Limit |
|---|---|
/api/auth/login |
5/min (IP) |
/api/auth/register |
3/hour (IP) |
/api/posts POST |
10/min (user) |
/api/search |
60/min (IP) |
| general GET | 300/min (IP) |
4. CORS — origin whitelist
const ALLOWED_ORIGINS = new Set(["https://example.com"]);
const origin = req.headers.get("origin") ?? "";
if (ALLOWED_ORIGINS.has(origin)) {
headers.set("Access-Control-Allow-Origin", origin);
headers.set("Access-Control-Allow-Credentials", "true");
}
Never combine * with Allow-Credentials: true.
5. Security headers (Caddy)
(security) {
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
}
}
6. CSP
default-src 'self';
script-src 'self' 'nonce-xxx';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
Start with Content-Security-Policy-Report-Only to collect violations, then enforce.
7. Cookie flags
res.cookies.set("session", token, {
httpOnly: true, secure: true, sameSite: "lax",
maxAge: 60 * 60, path: "/",
});
sameSite: strict— safest but breaks follow-from-another-tabsameSite: lax— practical defaultsameSite: none— third-party cookie, must besecure
8. CSRF beyond sameSite
// double-submit cookie
res.cookies.set("csrf_token", csrfToken, { httpOnly: false });
// frontend sends that cookie as a header; server compares
9. Clickjacking
X-Frame-Options: SAMEORIGIN or frame-ancestors 'none' in CSP.
10. Gotchas
Allow-Origin: *with credentials- CSP missing → XSS blast radius
- Manual Nginx TLS cert renewal mistakes. Caddy auto-renews.
- IP-only rate limits punish NAT users — combine with user_id
Closing
Caddy + CSP + sameSite cookies form a safe default. Verify defaults rather than adding exotic settings.
Next
- 06-anonymous-form-hardening