학습 목표
REST/JSON API에서 인증·인가 다음에 필요한 운영·경계 통제를 설명할 수 있다.
rate limiting 알고리즘·키 단위·429 응답 설계를 설명할 수 있다.
스키마 검증으로 Injection·mass assignment·타입 혼동을 경계에서 차단하는 이유를 설명할 수 있다.
CORS가 브라우저 정책임을 설명하고 preflight·credentials 오설정 위험을 설명할 수 있다.
API 게이트웨이·앱·WAF에서 defense in depth 역할을 구분할 수 있다.
문제 상황
- 공개 API 키 없이 호출 — 스크래퍼가 분당 수천 요청·DB 고갈
POST /users에{"role":"admin"}추가 — mass assignment로 권한 상승 (11편)page=999999&size=1000000— OOM·타임아웃 (DoS)Origin: https://evil.com인데Access-Control-Allow-Origin: *+ 쿠키 — 브라우저 정책 오해Content-Type: text/plain으로 JSON 파서 우회 시도- 모바일·서버는 CORS 무관 — “CORS 막았으니 안전” 착각
- 13편 API 키를 헤더에 넣었지만 rate limit 없음 — 유출 시 무제한 악용
Part 4 운영 보안 — 시크릿 다음은 API 경계에서 남용·잘못된 입력·브라우저 정책을 다룬다.
본 글 예제는 방어 설계 목적이다. 허가 없는 API 남용·침투는 불법이다.
1. API 위협 표면
API — 기계 간 계약. 웹 UI보다 자동화·병렬 호출이 쉬워 남용·스캔이 빈번하다.

| 계층 | 통제 | |
|---|---|---|
| Edge | WAF, DDoS, TLS | Network·Platform |
| Gateway | rate limit, API key, mTLS | Kong, ALB, Ingress |
| AuthN/Z | JWT, OAuth, RBAC | 5~8편 |
| App | schema validation, business rule | 본편 |
| Data | prepared statement, least privilege | 9편 |
클라이언트 → (TLS) → gateway rate limit → auth → validation → handler → DB
OWASP API Security Top 10 — BOLA(객체 권한), unrestricted resource consumption, unsafe consumption of APIs 등 — 본편은 소비 제한·입력·CORS에 초점.
2. Rate limiting
목적 — 가용성(DoS·비용)·공정 사용·유출 키 blast radius 축소.

| 알고리즘 | 동작 | |
|---|---|---|
| Fixed window | 분당 N회 카운터 | 경계에서 burst 2N 가능 |
| Sliding window | 최근 T초 이동 합 | 더 균일 |
| Token bucket | 버킷 충전·소비 | burst 허용 + 평균 상한 |
| Leaky bucket | 고정 속도 배출 | smooth 출력 |
| 키 | 적합 |
|---|---|
| IP | 익명·공개 API (NAT 공유 주의) |
| API key / client_id | B2B·파트너 |
| user_id | 로그인 후 공정 할당 |
| route + key | heavy endpoint 별도 한도 |
# code/rate-limit-middleware.example.py
# Token-bucket sketch (Redis INCR + TTL or dedicated lib)
LIMIT = 100
WINDOW_SEC = 60
def check_rate_limit(redis, key: str) -> bool:
count = redis.incr(f"rl:{key}")
if count == 1:
redis.expire(f"rl:{key}", WINDOW_SEC)
return count <= LIMIT
# HTTP 429 Too Many Requests
# Retry-After: 30
# X-RateLimit-Remaining: 0
| 응답 | |
|---|---|
| 429 | 한도 초과 — Retry-After 헤더 |
| 503 | upstream 과부하 — rate limit과 구분 |
| 헤더 | X-RateLimit-Limit, Remaining, Reset (관례) |
- Gateway에서 먼저 — 앱 도달 전 차단 (비용↓)
- 비용 민감 API — LLM·결제·SMS per-call 한도
- 13편 키 유출 — limit + 즉시 revoke + rotate
3. Input validation
경계에서 거부 — handler·ORM 전에 스키마로 형식·범위 고정.

| 원칙 | |
|---|---|
| Allowlist | enum·regex·known fields — denylist만 불충분 |
| 타입 | string vs number — "123" vs 123 혼동 방지 |
| 크기 | body max, array length, string length |
| 필드 | additionalProperties: false — mass assignment 방지 |
| 정규화 | Unicode NFKC, trim — 우회 축소 |
# code/input-validation-safe.py
from pydantic import BaseModel, Field, ConfigDict
class CreateUserRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
email: str = Field(max_length=254)
name: str = Field(min_length=1, max_length=100)
# role is NOT client-writable — set server-side only
def create_user(body: CreateUserRequest):
role = "member" # never trust body.role
...
# code/input-validation-vulnerable.py
# VULNERABLE — do not use
def create_user(body: dict):
user = User(**body) # role=admin from client
db.save(user)
| OWASP 연계 | |
|---|---|
| Injection (9편) | 검증 + prepared statement |
| BAC / mass assignment (11편) | 허용 필드만 바인딩 |
| SSRF (12편) | URL allowlist·private IP 차단 |
- 클라이언트 검증 — UX용; 서버 검증 필수
- GraphQL — query depth·complexity limit 별도
- 파일 업로드 — MIME sniff·size·virus scan
4. CORS
CORS(Cross-Origin Resource Sharing) — 브라우저가 다른 Origin 응답을 노출할지 말지 정하는 정책. 서버 간 호출에는 적용 안 됨.

| 헤더 | |
|---|---|
Access-Control-Allow-Origin |
허용 origin (와일드카드 주의) |
Access-Control-Allow-Methods |
GET, POST, … |
Access-Control-Allow-Headers |
Authorization, Content-Type |
Access-Control-Allow-Credentials |
쿠키·인증 헤더 |
Access-Control-Max-Age |
preflight 캐시 |
Simple GET (no preflight): browser adds Origin → server reflects Allow-Origin
Non-simple (JSON POST + Authorization):
1. OPTIONS preflight
2. server 204 + Allow-* headers
3. actual request
# code/cors-nginx.example
# Reflect allowed origin — never * with credentials
set $cors_origin "";
if ($http_origin ~* ^https://(app|admin)\.example\.com$) {
set $cors_origin $http_origin;
}
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Vary Origin always;
| 오설정 | 위험 |
|---|---|
Allow-Origin: * + credentials |
브라우저 거부 (스펙) |
모든 Origin 반사 ($http_origin 무비판) |
악성 사이트 허용 |
| CORS만 믿고 인가 생략 | curl·Postman은 CORS 무시 |
| preflight 캐시 과다 | 정책 변경 지연 반영 |
- API 전용 (모바일·서버 only) — CORS 불필요; 인증·인가가 핵심
- 쿠키 세션 —
SameSite·CSRF (10편) + CORS 함께 - Network TLS (3편) — CORS 대체 아님
5. API 키·버전·문서
| 통제 | |
|---|---|
| API key | 헤더 X-API-Key — 13편 rotation·scope |
| OAuth scope | fine-grained 리소스 접근 (6~7편) |
| Versioning | /v1 — breaking change 격리 |
| Deprecation | Sunset 헤더·문서 기한 |
| Error body | 스택·내부 ID 노출 금지 (12편 misconfig) |
# Minimal safe error
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{"type":"validation_error","title":"Invalid request","status":400}
6. 체크리스트
| 항목 | 확인 |
|---|---|
| Rate limit | gateway + app, per key/user/route |
| 429 | Retry-After, monitoring on throttle |
| Schema | OpenAPI/Pydantic, extra fields forbidden |
| Size | max body, pagination cap |
| CORS | explicit allowlist, no blind origin reflect |
| Auth | every endpoint — no “public by omission” |
| Keys | vault, rotate, per-env (13편) |
| Logs | no full API key in access log |
| 안티패턴 | |
|---|---|
| UI만 검증 | 서버 우회 trivial |
| CORS = security | 서버 인가 없으면 무의미 |
| 무제한 list API | limit=100 cap |
| rate limit only IP | 공유 NAT 차단 과다 |
7. 정리
- API — 자동화·남용에 취약; gateway·앱 다층 방어
- Rate limit — 가용성·비용·유출 키 피해 제한
- Validation — 경계에서 스키마·allowlist; Injection·BAC 연계
- CORS — 브라우저 정책; 인가 대체 불가
- 13편 시크릿 + 5~8편 인증·인가 + 본편 운영 통제 = API 최소 세트
다음에 다룰 것
- 로깅과 감사
- PII 마스킹, Observability 연계
해당 내용은 OWASP API Security Top 10, OWASP REST Security Cheat Sheet 의 내용을 기반으로 합니다.
'애플리케이션 보안' 카테고리의 다른 글
| 보안 체크리스트 (0) | 2026.07.03 |
|---|---|
| 로깅과 감사 (0) | 2026.07.02 |
| 시크릿 관리 (0) | 2026.06.30 |
| OWASP Top 10 (0) | 2026.06.29 |
| Broken Access Control (0) | 2026.06.28 |