"CORS 는 보안 아냐. 브라우저가 server-선언 정책 강제하는 것. 일단 내재화하면 '왜 CORS 어려워' 혼란 절반 사라지고 — 나머지 절반이 'server 가 뭘 선언하고 있는지에 대해 내가 틀림' 됨."
CORS 풀어주는 mental model
CORS 혼란 대부분 fix 하는 사실 둘:
- CORS 는 브라우저만 강제. curl request, Python httpx 호출, server-to-server fetch — 어느 것도 CORS 안 신경. 체크가 브라우저의 네트워크 층에서 일어남; 브라우저 아닌 HTTP client 가 완전 무시.
- Server 가 정책 선언; 브라우저가 강제. Server 가 "이 origin 에서, 이 method 로, 이 header 와 함께 호출 받을 의향" 말하는
Access-Control-Allow-*response header 추가. 브라우저가 그 header 읽고 request 만든 JavaScript 한테 response 노출할지 결정.
둘 다 동시 들고 있을 수 있으면 CORS 버그가 'server 가 내가 생각하는 거 선언 안 하고 있음' 됨 — configuration 문제, 미스터리 아님.
Origin 이 뭐 의미
Origin 이 triple (scheme, host, port). https://example.com, https://example.com:443, http://example.com 가 다른 origin 셋. 다른 거 cross-origin 이면 CORS 춤 필요.
Same-Origin Policy (SOP) 가 브라우저 기본: origin A 의 JavaScript 가 origin B 의 response 못 읽음. 없으면 악성 사이트가 너의 은행 로그인 데이터 읽을 수 있음. CORS 가 통제된 완화 — server B 가 "origin A 가 나 읽도록 허용" 말하고 브라우저가 존중.
Simple request vs preflighted request
브라우저가 cross-origin request 를 두 카테고리로 나눔.
Simple request (preflight 불필요) — 다 만족:
- Method 가 GET, HEAD, POST.
- Header 가 작은 "CORS-safelisted" set (Accept, Accept-Language, Content-Language, 몇 값 가진 Content-Type).
- Content-Type 이
application/x-www-form-urlencoded,multipart/form-data,text/plain중 하나.
브라우저가 request 정상 보냄; response 에 Access-Control-Allow-Origin: matching-origin 있으면 JS 한테 response 노출. 아니면 response 차단.
Preflighted request (나머지 다, Content-Type: application/json 가진 POST 포함):
- 실제 request 전 브라우저가 같은 URL 에
Origin,Access-Control-Request-Method,Access-Control-Request-Headers가진OPTIONSrequest 보냄. - Server 가 허용 선언하는
Access-Control-Allow-Origin/Methods/Headers로 응답. - 브라우저가 비교. 실제 request 가 선언 정책 안에 맞으면 브라우저 진행. 아니면 차단.
- 실제 request 보냄; response 도
Access-Control-Allow-Origin필요.
Preflight 가 round trip 하나 추가. 브라우저가 preflight 결과를 몇 초 캐시 (Access-Control-Max-Age response header) 라서 반복 호출이 재-preflight 안 함.
핵심 header 다섯
Access-Control-Allow-Origin— server 가 허용하는 specific origin (혹은*와일드카드). Allowlist 에 있으면 요청 origin echo; 임의 origin 절대 echo 마.Access-Control-Allow-Credentials— 브라우저가 cookie 와 Authorization header 포함하길 원하면true설정. 핵심:Access-Control-Allow-Origin: *와 조합 못 함 — 와일드카드와 credentials 상호 배타.Access-Control-Allow-Methods— 이 URL 에 받는 method (preflight 만).Access-Control-Allow-Headers— client 가 보낼 수 있는 request header (preflight 만).Access-Control-Max-Age— 브라우저가 preflight 결과 캐시할 시간.
와일드카드 + credentials 함정
가장 흔한 CORS 버그 둘:
1. Access-Control-Allow-Origin: * + cookie. 브라우저 거부. 와일드카드가 "어느 origin 이든 나 읽음" 의미, 자격 증명 보내는 거랑 호환 안 됨. Fix: 와일드카드 대신 (allowlist 에 있나 확인 후) specific 요청 origin echo.
2. Preflight 가 204 돌려주는데 CORS header 누락. Preflight 가 HTTP-200, 근데 Access-Control-Allow-Origin 선언 안 함, 그래서 브라우저가 어쨌든 차단. 항상 OPTIONS response 에 header 포함, 실제 request response 만 말고.
cwkPippa 의 CORS 현실
backend/main.py 의 _allowed_origins 가 정확히 http://localhost:5173, http://127.0.0.1:5173, Tailscale IP origin 담음. 와일드카드 없음; credentials 허용 (cookie + Authorization). FastAPI 의 CORSMiddleware 가 preflight 춤 자동 처리 — handler 안 쓰고 모든 endpoint 가 OPTIONS 지원. 새 device 추가는 그 Tailscale IP 를 목록에 추가하고 재시작 의미. CLAUDE.md gotcha 섹션이 이거 정확히 호출 — 새 instance 의 내가 CORS allowlist 먼저 안 확인하고 "이 IP 에서 API 안 됨" debug 하려 해서.