[WEB] 브라우저의 CORS 정책
CORS(Cross-Origin Resource Sharing)
CORS(Cross-Origin Resource Sharing)
란 웹 브라우저에서 교차(다른) 출처의 리소스를 공유하기 위해 만들어진 http 기반 요청/응답 매커니즘이다.
우선 CORS를 이해하기 앞서 동일 출처와 교차 출처에 대해 알아야 한다.
출처(Origin)란 http://localhost:8080/userid?hello
와 같은 URL 구조에서 프로토콜, 호스트, 포트에 해당되는 http://localhost:8080
까지를 말한다.
URL | 같은 출처 | 이유 |
---|---|---|
https://tistory.com/write | O | Protocal, Host, Port 동일 |
https://tistory.com/write?id=15 | O | Protocal, Host, Port 동일 |
https://tistory.com/write#work | O | Protocal, Host, Port 동일 |
http://tistory.com/write | X | Protocal 다름 |
https://tistory.co.kr/write | X | Host 다름 |
https://tistory.com:81/write | X | Port 다름 |
동일 출처와 교차 출처의 구분은 사용자가 브라우저에 최초로 요청한 주소를 기준으로 판단한다. 이때 웹 페이지를 실행하는 과정에서 추가로 발생하는 요청의 출처(Origin)가 최초 요청 주소의 출처와 동일한지 다른지를 비교한다.
⊙ ⊙ ⊙
CORS의 탄생
CORS는 SOP의 엄격한 제한을 필요한 경우에 안전하게 완화할 수 있도록 하는 확장 메커니즘이다.
브라우저는 기본적으로 동일 출처 정책(SOP, Same-Origin Policy)을 따른다.
동일한 출처의 리소스 접근만을 허용한다.
동일 출처 정책(SOP, Same-Origin-Policy)이란 쉽게 말해 브라우저가 실행되는 동안 다른 서버의 요청을 허용하지 않고 동일한 서버의 요청에 한해서만 허용하겠다는 의미이다. 동일 출처 정책은 CSRF(Cross-Site Request Forgery)나 XSS(Cross-Site Scripting) 등의 보안 취약점을 노린 공격을 방어할 수 있다.
하지만 SOP는 웹 서비스 개발에 있어 너무 엄격한 제약으로 현실적으로 많은 문제를 유발하게 된다. 웹 서비스 개발에 필요한 다양한 외부 서비스들을 사용하는 데에 어려움이 발생한다. 그래서 이를 보완하기 위해 CORS가 생기게 되었다. 따라서 외부 리소스가 필요한 경우 CORS 매커니즘을 통해 외부 리소스를 공유할 수 있게 되었다.
CORS는 브라우저의 구현 스펙에 포함되는 정책이기 때문에, 브라우저를 통하지 않고 서버 간 통신을 할 때는 이 정책이 적용되지 않는다.
⊙ ⊙ ⊙
CORS 실행과 동작 원리
브라우저는 교차 출처 요청이 발생하는 경우 CORS 매커니즘을 자동으로 실행한다.
기본적으로 웹 클라이언트 어플리케이션이 교차 출처의 리소스를 요청할 때는 HTTP 프로토콜을 사용하여 요청을 보내게 되는데, 이때 브라우저는 요청 헤더에 Origin
이라는 필드에 요청을 보내는 출처를 함께 담아보낸다.
Origin: https://origin.com
이후 서버가 이 요청에 대한 응답을 할 때 응답 헤더의 Access-Control-Allow-Origin
이라는 값에 “이 리소스를 접근하는 것이 허용된 출처”를 내려주고, 이후 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin을 비교해본 후 이 응답이 유효한 응답인지 아닌지를 결정한다.
CORS가 동작하는 방식은 크게 단순 요청(Simple Request), 사전 요청(preflight Request), 인증된 요청(Credentialed Request)으로 나뉜다. 세 가지 요청에 대해 알고 있다면 내가 보낸 요청이 어떤 시나리오에 해당되는지 잘 파악하여 CORS 정책 위반으로 인한 에러를 고치는 것이 한결 쉬울 것이다.
CORS의 세 가지 동작 방식
CORS 요청/응답 헤더 목록
요청 헤더
Origin
Access-Control-Request-Method
Access-Control-Request-Headers
응답 헤더
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Allow-Credentials
Access-Control-Expose-Headers
Access-Control-Max-Age
사전 요청 (preflight request)
사전 요청(preflight request)은 실제 요청을 보내기 전에 해당 요청이 안전한지 확인하기 위해 브라우저가 사전에 먼저 보내는 요청을 말한다. 모든 교차 출처에 대해 사전 요청을 보내는 것은 아니다. 특정 조건(이하 Preflight 요청 조건)에 해당되는 경우에 사전 요청을 보낸다.
preflight 요청의 상세 동작 방식
1. 브라우저는 서버에게 preflight 요청을 보낸다.
- 교차 출처 요청이 preflight 요청 조건에 해당하면, 브라우저는 실제 요청 전에 http 메서드 중 하나인 OPTIONS 메서드를 사용하여 서버에 허가를 요청한다.
- preflight 요청에는 실제 요청과는 다르게 다음과 같은 헤더 정보만을 포함된다.
Origin
: 요청의 출처Access-Control-Request-Method
: 실제 요청에서 사용할 HTTP 메서드Access-Control-Request-Headers
: 실제 요청에서 사용할 사용자 정의 헤더 목록
요청 예시:
OPTIONS /api/resource HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
2. 요청을 받은 서버는 자신의 허용 정보를 담아 브라우저에 응답한다.
- 서버는 preflight 요청에 대한 응답으로 현재 자신이 어떤 것들을 허용하고, 어떤 것들을 금지하고 있는지에 대한 정보를 응답 헤더에 담아서 브라우저에게 다시 보내주게 된다.
Access-Control-Allow-Origin
: 허용된 출처Access-Control-Allow-Methods
: 허용된 HTTP 메서드Access-Control-Allow-Headers
: 허용된 헤더Access-Control-Max-Age
: 허용 결과를 캐싱할 시간(초)
예시 응답:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 3600
3. **브라우저는 실제 요청을 보낸다.
- 브라우저는 자신이 보낸 preflight 요청과 서버가 응답에 담아준 허용 정책을 비교한 후, 실제 요청을 보내는 것이 안전하다고 판단되면 같은 엔드포인트로 다시 실제 요청을 보내게 된다.
Preflight 요청 조건
다음 조건 중 하나라도 해당되면 브라우저는 반드시 preflight 요청을 보낸다.
- HTTP 메서드 중
GET, HEAD, POST
이 아닌 다른 메서드를 사용하는 경우- 예:
PUT, DELETE
를 사용하면 preflight 요청을 보낸다.
- 예:
- 헤더에 CORS-safelisted 헤더(Accept, Accept-Language, Content-Language, Content-Type) 외에 다른 헤더를 사용하는 경우
- 예:
Authorization, RefreshToken, Custom-Header
등을 사용하면 preflight 요청을 보낸다.- CORS-safelisted 헤더 목록
- Accept
- Accept-Language
- Content-Language
- Content-Type (단, 허용되는 값으로 제한됨)
- Range
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
- 예:
- Content-Type 헤더가 다음 셋이 아닌 경우:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- 예:
application/json, application/xml
사용 시 preflight 요청을 보낸다.
단순 요청(Simple Request)
단순 요청은 사전 요청없이 처음부터 서버에 보낼 실제 요청을 보내는 방식을 말한다. 요청 시 헤더에 출처(Origin) 정보를 담아서 전송한다. **
단순 요청 동작 과정
- 브라우저는 요청 헤더에 Origin 정보를 담아서 실제 요청을 서버에 전송한다.
- 요청을 받은 서버는 CORS 응답 헤더에 허가 정보와 함께 실제 요청에 대한 응담 정보도 같이 보낸다.
- 응답을 받은 브라우저는 CORS 정보와 자신의 Origin 정보를 비교하여 CORS 정책 검증에 통과한 경우 서버가 보낸 응답 데이터를 사용한다. 검증에 실패하면 응답 데이터를 사용을 차단한다.
단순 요청 조건
아무 때나 단순 요청을 사용할 수 있는 것은 아니고, 특정 조건을 만족하는 경우에만 예비 요청을 생략할 수 있다. 게다가 이 조건이 조금 까다롭기 때문에 일반적인 방법으로 웹 어플리케이션 아키텍처를 설계하게 되면 거의 충족시키기 어려운 조건들이다.
- 요청의 메소드는
GET
,HEAD
,POST
중 하나여야 한다. - Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width를 제외한 헤더를 사용하면 안된다.
- 만약 Content-Type를 사용하는 경우에는 application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용된다.
사실 1번 조건의 경우는 그냥 PUT이나 DELETE 같은 메소드를 사용하지 않으면 되는 것 뿐이니 그렇게 보기 드문 상황은 아니지만, 2번이나 3번 조건 같은 경우는 조금 까다롭다.
애초에 조건에 명시된 헤더들은 정말 기본적인 헤더들이기 때문에, 복잡한 상용 웹 어플리케이션에서 이 헤더들 외에 추가적인 헤더를 사용하지 않는 경우는 거의 없고, 당장 사용자 인증에 사용되는 Authorization
헤더 조차 조건에는 포함되지 않는다.
3번 조건은 많은 REST API들이 Content-Type으로 application/json을 사용하기 때문에 지켜지기 어려운 조건이다.
단순 요청 vs 사전 요청
구분 | 단순 요청(Simple Request) | 사전 요청(Preflight Request) |
---|---|---|
요청 방식 | 바로 실제 요청 전송 | 먼저 OPTIONS 요청으로 허용 여부 확인 |
데이터 포함 | 요청에 데이터 포함 | 데이터 없이 메타 정보(Method , Headers )만 포함 |
Origin 포함 | 요청에 포함 | 포함 |
서버 응답 | 실제 데이터 + CORS 정보 | CORS 허용 정보만 포함 |
브라우저 판단 | 응답의 CORS 정보 검증 후 데이터 사용 | 허용되면 이후 실제 요청 진행 |
댓글남기기