포스트

SOP(Same-Origin Policy) vs CORS(Cross-Origin Resource Sharing)

Origin(프로토콜·도메인·포트) 판별 기준부터 브라우저의 기본 보안 정책인 SOP, 이를 우회하는 CORS 응답 헤더와 Preflight 요청 동작까지 정리한다.

SOP(Same-Origin Policy) vs CORS(Cross-Origin Resource Sharing)

Same Origin vs Cross Origin

  • Origin은 프로토콜, 도메인, 포트로 구성되어 있다.
  • Same-Origin은 이 세 가지가 모두 동일한 경우를 말하며, Cross-Origin은 이 세 가지 중 하나라도 다른 경우를 말한다.
출처1출처2Same-Origin?
http://example.comhttp://example.comtrue
http://example.comhttp://example.com:8080false
http://example.comhttps://example.comfalse
http://example.comhttp://sub.example.comfalse

SOP(Same-Origin Policy)

  • SOP는 웹 브라우저가 다른 Origin의 리소스에 접근하는 것을 제한하는 기본 보안 메커니즘이다.
  • SOP는 Cross-Origin 요청을 제한하여 CSRF, XSS 등의 공격을 방지한다.

CORS(Cross-Origin Resource Sharing)

  • CORS는 위 SOP정책의 예외로 Cross-Origin 요청을 허용하는 메커니즘이다.
  • 서버에서 허용하는 Origin을 설정을 헤더를 통해 응답하고, 브라우저에서 응답 헤더의 Origin을 체크하여 Cross-Origin 요청을 허용한다.
1
Access-Control-Allow-Origin: <origin> # Access-Control-Allow-Origin: http://example.com, https://example.com
헤더설명
Access-Control-Allow-Origin허용하는 Origin을 설정
Access-Control-Allow-Methods허용하는 메서드를 설정
Access-Control-Allow-Headers허용하는 헤더를 설정
Access-Control-Max-AgePreflight Request의 캐싱 시간을 설정

origin 정보가 없는 경우 나머지 헤더는 무시된다.

Simple Request vs Preflight

CORS 요청이라고 항상 OPTIONS 사전 요청이 붙는 건 아니다. 조건을 모두 만족하면 preflight 없이(=Simple Request) 바로 나가고, 하나라도 벗어나면 브라우저가 OPTIONS를 먼저 보낸다. 이 경계를 모르면 “왜 어떤 요청은 OPTIONS가 두 번 찍히고 어떤 건 안 찍히나”에서 헤맨다.

Simple Request 조건 (전부 충족해야 함)

  • 메서드가 GET, HEAD, POST 중 하나
  • 수동 지정 헤더가 Accept, Accept-Language, Content-Language, Content-Type 등 안전 목록(CORS-safelisted)뿐
  • Content-Typeapplication/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나

Preflight를 유발하는 대표 케이스

  • Content-Type: application/json → 대부분의 REST/JSON API가 여기 걸려 항상 preflight가 붙는다
  • Authorization, X-Requested-With 같은 커스텀 헤더 추가
  • PUT, DELETE, PATCH 메서드
1
2
3
4
OPTIONS /api/orders          # ① 브라우저가 먼저 보내는 preflight
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization

서버가 이 OPTIONS에 허용 Origin·메서드·헤더를 응답해야 ② 실제 POST가 나간다. Access-Control-Max-Age로 이 사전 요청 결과를 캐싱하면 매 요청마다 OPTIONS가 붙는 걸 줄인다.

credentials(쿠키·인증) 포함 요청

쿠키나 Authorization을 실어 보내려면 클라이언트가 credentials: 'include'(fetch) / withCredentials = true(axios)를 켜야 하고, 서버는 Access-Control-Allow-Credentials: true를 응답해야 한다. 여기서 가장 많이 밟는 함정:

  • credentials 요청에서는 Access-Control-Allow-Origin: *(와일드카드)가 금지된다. 반드시 정확한 Origin 하나를 돌려줘야 한다 → 요청의 Origin 헤더를 읽어 화이트리스트 검사 후 그대로 반사(reflect)하는 패턴을 쓴다.
  • Access-Control-Allow-Headers, Access-Control-Allow-Methods도 credentials 요청에선 *가 리터럴 *로 해석되어 안 통한다.

실제 에러 메시지와 원인

브라우저 콘솔의 CORS 에러는 문구마다 원인이 다르다.

콘솔 메시지(요지)실제 원인
No 'Access-Control-Allow-Origin' header is present서버가 CORS 헤더를 아예 안 붙임. OPTIONS 라우트가 없거나 에러(4xx/5xx)를 반환 중
...Allow-Origin header has a value '...' that is not equal to the supplied origin화이트리스트에 요청 Origin이 없음. 포트·프로토콜(http/https)·서브도메인까지 정확히 일치해야 함
...does not pass access control check: It does not have HTTP ok statuspreflight(OPTIONS)에 인증 미들웨어가 끼어 401/403을 반환. OPTIONS는 인증 예외 처리 필요
Credentials flag is 'true', but 'Access-Control-Allow-Origin' is '*'위 credentials + 와일드카드 충돌

자주 밟는 함정

  • CORS는 브라우저만의 규칙이다. 서버 간 호출, curl, Postman에는 적용되지 않는다. “Postman은 되는데 브라우저는 안 된다”는 정상이다.
  • localhost:3000 → localhost:8080도 Cross-Origin이다. 도메인이 같아도 포트가 다르면 CORS 대상이다. 로컬 개발에서 프런트/백 포트가 갈리면 반드시 겪는다.
  • OPTIONS가 인증에 막히는 문제가 제일 흔하다. 프레임워크의 CORS 필터를 인증 필터보다 에 둬야 preflight가 통과한다(Spring Security라면 cors()를 시큐리티 체인 앞단에).
  • 커스텀 응답 헤더(예: X-Total-Count)를 JS에서 읽으려면 Access-Control-Expose-Headers에 명시해야 한다. 안 하면 응답엔 있는데 response.headers.get()null이다.

MDN SOP
MDN CORS

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.