codingstairs
NotesEDULifeContact
⌕Search⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

Get in touch

Send without signing in. Add your email if you'd like a reply.

  • Leave a message anonymously →
  • ✉ warragon112@gmail.com
  • KakaoTalk Open Chat ↗

© 2026 codingstairs

  • Notes
  • EDU
  • Search
  • Life
  • Contact
  • Legal
  • RSS
  • GitHub
EDU›Web security foundations — JWT · OAuth · OWASP›Step 5

Step 5

Rate limit + CORS + security headers

0 views

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-tab
  • sameSite: lax — practical default
  • sameSite: none — third-party cookie, must be secure

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

← Step 4

Input validation + length caps

Step 6 →

Anonymous form hardening