개발자들을 괴롭히는 SOP(동일 출처 정책)와 CORS(교차 출처 리소스 공유)
CORS 에러는 프론트엔드-백엔드 간 협업을 곤란하게 하는 악의 축이다
나도 관통프로젝트 할 때 CORS 에러에 의해 앓아누울 뻔 한 적이 있었다가 구글링을 통해 해결했는데, 생각해보니 CORS 에러가 무엇인지 어떤 의미인지에 대해서는 몰랐었기에
https://www.youtube.com/watch?v=bW31xiNB8Nc
https://www.youtube.com/watch?v=-2TgkKYmJt4
위의 두 영상을 보고 CORS, 그리고 CORS 에러의 원인인 SOP 정책에 대해 공부하게 되었다.
SOP(Same-Origin Policy)란 어떤 출처(프로토콜, 호스트, 포트의 조합을 한 출처라고 한다)에서 불러온 문서, 스크립트, 리소스가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 보안 방식이다
이러한 정책은 잠재적으로 해로울 수 있는 문서를 분리하여 공격받을 수 있는 경로는 줄이는 것을 의미한다(MDN)
이러한 식으로 프로토콜, 호스트, 포트 중 하나라도 다르면 브라우저는 같은 출처라고 보지 않는다.
다른 출처에 대한 접근 제한을 막지 않을 시 아래와 같은 상황이 발생할 수 있다
1️⃣ 사용자가 웹 사이트에 로그인을 시도한다
2️⃣ 로그인 성공과 함께 인증 토큰을 발급받아 쿠키에 저장한다
3️⃣ 외부에서 사용자에게서 인증 토큰의 탈취를 시도한다(ex. 이메일 클릭 시 script 실행)
4️⃣ 사용자의 인증 토큰을 뺏어온다
5️⃣ 웹 사이트를 통해 인증 토큰을 가진 다른 출처로 접근하여 서비스를 이용할 수 있다(해킹 위험)
이 때, 동일한 인증 토큰으로 접근하더라도 사용자의 출처(ex. 192.173.0.30:8080 vs http://unknown.com:8092)는 다른 른 출처를 가지고 있기에, 다른 출처로의 접근을 제한하는 것이 SOP 보안 방식이다.
하지만 개발을 하다보면 다른 출처로서의 리소스를 허용해야 하는 순간이 반드시 있기 때문에 CORS를 사용한다.
CORS(Cross-Origin Resource Sharing)은 추가 HTTP 헤더를 사용하여, 한 출처에서 실행중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다.
CORS를 허용하면 웹 페이지는 교차 출처 이미지, 스타일시트, 스크립트, 동영상 등의 자원들을 임베드하여 사용할 수 있다.
SOP에 의해 Ajax 요청이 기본적으로 금지되기 때문에 Ajax를 사용할 때 CORS 에러를 자주 접할 수 있다.
CORS가 동작하는 방식에는 크게 세 가지가 있지만 여기에서는 두 가지 방식만 간단하게 살펴보기로 한다.
1. Simple Requests
다음의 요청들을 모두 성립해야 한다
1️⃣ 특정 HTTP Method(GET/HEAD/POST)를 사용
2️⃣ 자동으로 설정된 헤더 이외에는 다음의 헤더만 사용 가능
- Accept
- Accept-Language
- Content-Launguage
- Content-Type
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
3️⃣ Content-Type 헤더는 다음의 값들만 허용
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
(이외에도 두 가지 조건이 더 있지만 생략한다)
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
브라우저가 위와 같이(헤더의 Origin을 명시) 서버로 요청을 보내면
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
[…XML Data…]
서버는 Access-Control-Allow-Origin 헤더를 사용하여 응답한다.
이 때에는, 모든 도메인에서 접근할 수 있음을 의미하며 특정 출처만 허용하려는 경우에는 Access-Control-Allow-Origin에 해당 도메인을 명시하여 보낸다.
2. Preflighted Request
simple request와는 달리, OPTIONS 메소드를 통해 다른 도메인의 리소스로 HTTP 요청을 보냈을 때 전송이 안전한지 확인하는 방식이다.
위처럼 Preflight에서의 전송이 안전하게 확인되면 메인 요청을 전달하는 방식으로 사용한다.
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
위와 같이 preflight request가 완료되면, 실제 요청을 전송한다.
POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: https://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: https://foo.example
Pragma: no-cache
Cache-Control: no-cache
<person><name>Arun</name></person>
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain
[Some GZIP'd payload]
3. Request with Credentials
이후 추가 예정
앞으로 시작할 공통PJT에서도 CORS 에러때문에 고통받는 일이 없었으면 좋겠다 ㅠ
참고 자료
https://developer.mozilla.org/ko/docs/Web/Security/Same-origin_policy
https://developer.mozilla.org/ko/docs/Web/HTTP/CORS
https://en.wikipedia.org/wiki/Cross-origin_resource_sharing
https://ko.wikipedia.org/wiki/%EB%8F%99%EC%9D%BC-%EC%B6%9C%EC%B2%98_%EC%A0%95%EC%B1%85