codingstairs
노트에듀라이프연락
⌕검색⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

연락하기

로그인 없이도 보낼 수 있어요. 답변이 필요하면 이메일을 함께 적어 주세요.

  • 익명 폼으로 의견 남기기 →
  • ✉ warragon112@gmail.com
  • 카카오톡 오픈채팅 ↗

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
에듀›웹 보안의 기초 — JWT · OAuth · OWASP›5단계

5단계

Rate limit + CORS + 보안 헤더

0회 조회

Rate limit + CORS + 보안 헤더

3 자리가 깔끔하면 대부분의 자동 공격은 막습니다.

1. Rate limit — sliding window (Redis)

import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL!);

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 };
}

사용:

export async function POST(req: Request) {
  const ip = req.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? "anon";
  const { allowed } = await rateLimit(`login:${ip}`, 5, 60);
  if (!allowed) return NextResponse.json({ error: "rate_limit" }, { status: 429 });
  // ... 로그인 처리
}

분당 5 회 제한.

2. Redis 없이 — PostgreSQL

SELECT count(*) FROM rate_limit_events
WHERE key = $1 AND created_at > now() - interval '1 minute';

장점: 의존성 감소. 단점: 쓰기 부하 증가.

3. 경로별 한도

경로 한도
/api/auth/login 분당 5 (IP)
/api/auth/register 시간당 3 (IP)
/api/posts (POST) 분당 10 (user)
/api/search 분당 60 (IP)
일반 GET 분당 300 (IP)

쓰기는 엄격 · 읽기는 느슨.

4. CORS — Origin 화이트리스트

// Next.js middleware 또는 API route 내
const ALLOWED_ORIGINS = new Set([
  "https://example.com",
  "https://admin.example.com",
]);

const origin = req.headers.get("origin") ?? "";
const headers = new Headers();
if (ALLOWED_ORIGINS.has(origin)) {
  headers.set("Access-Control-Allow-Origin", origin);
  headers.set("Access-Control-Allow-Credentials", "true");
  headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
  headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
}

if (req.method === "OPTIONS") return new Response(null, { status: 204, headers });

Access-Control-Allow-Origin: * + Allow-Credentials: true 조합 금지 (브라우저가 거부).

5. 보안 헤더 (Caddy or middleware)

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=()"
  }
}

example.com {
  import security
  reverse_proxy localhost:3000
}

6. CSP (Content Security Policy)

XSS 2 차 방어. 외부 스크립트 소스를 명시적 화이트리스트.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-xxx' https://www.google-analytics.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';

Next.js 에서는 middleware 로 nonce 생성 · inject. 초반에는 Content-Security-Policy-Report-Only 로 수집만 해서 위반 내역 분석 후 차단.

7. 쿠키 플래그

res.cookies.set("session", token, {
  httpOnly: true,      // JS 접근 불가
  secure: true,        // HTTPS 만
  sameSite: "lax",     // CSRF 기본 방어
  maxAge: 60 * 60,     // 1 시간
  path: "/",
});
  • sameSite: strict — 가장 안전하나 외부 링크 클릭 시 세션 소실
  • sameSite: lax — GET 만 cross-site 허용. 실용적 기본
  • sameSite: none — third-party cookie. secure 필수

8. CSRF 추가 방어 (sameSite 외)

sameSite: lax 가 대부분을 막지만 완전하지 않음.

// Double-submit cookie
res.cookies.set("csrf_token", csrfToken, { httpOnly: false });
// 프론트가 이 쿠키 값을 header 에도 함께 보냄
// 서버가 쿠키 값과 header 값 일치 확인

또는 Synchronizer Token Pattern (서버가 세션과 매칭하는 별도 토큰).

9. Clickjacking 방어

X-Frame-Options: SAMEORIGIN 또는 Content-Security-Policy: frame-ancestors 'none'. 다른 사이트가 iframe 으로 감싸지 못하게.

10. 자주 걸리는 자리

  • **Allow-Origin: *** — 자격증명 없으면 OK, 있으면 사고
  • CSP 없이 — XSS 피해 확산
  • Caddy 없이 Nginx 수동 — TLS 인증서 갱신 실수 잦음. Caddy 자동 추천
  • rate limit 의 IP 만 기준 — NAT 뒤 사용자 여러 명 공유. user_id 조합 권장

하고픈 말

Caddy + CSP + sameSite cookie 조합이 초보자에게도 안전한 기본값. 과잉 설정보다 기본값이 올바른지 점검하는 게 실수 줄이는 길.

Next

  • 06-anonymous-form-hardening

← 4단계

입력 검증 + 길이 상한

6단계 →

익명 폼 하드닝