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 | 출처2 | Same-Origin? |
|---|---|---|
| http://example.com | http://example.com | true |
| http://example.com | http://example.com:8080 | false |
| http://example.com | https://example.com | false |
| http://example.com | http://sub.example.com | false |
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-Age | Preflight 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-Type이application/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 status | preflight(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이다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.