동일 출처 정책(Same-Origin Policy)
사이드 프로젝트를 진행하다보면 자주 보는 것이 하나있는데 바로 CORS 에러다. CORS 에러는 브라우저의 동일 출처 정책(Same-Origin Policy)을 위반하면 발생하는 에러인데, 여기서 동일 출처란 프로토콜, 호스트(도메인), 포트가 모두 같은 것을 말한다. 'http://mypage:8080'에서 요청을 보낸다고 했을 때, 동일 출처 정책을 위반한 경우는 아래와 같다.
- https://mypage:8080/v1/api/product/list로 요청을 보낸 경우 > 프로토콜 불일치
- http://yourpage:8080/v1/api/product/list로 요청을 보낸 경우 > 호스트(도메인) 불일치
- https//mypage:8090/v1/api/product/list로 요청을 보낸 경우 > 포트 불일치
누가/어디서 막는걸까?
domain-b.com:8082에서 dev.mojee.me로 요청을 보냈으나, CORS 에러가 발생하며 응답값을 받아오지 못했다. 그렇다면 요청을 보내고 응답을 받는 과정 중 어디에서/누가 막는것일까? Wireshark를 통해 패킷을 보면, dev.mojee.me로 보낸 요청은 제대로 서버로 전달되었고, 그 응답역시 200 OK 로 잘 도착하였다. 결론적으로 dev.mojee.me로의 요청은 서버로 잘 전달되었고, 서버의 응답도 잘 도착하였다. 하지만 그 결과를 보여주어야할 브라우져가 이를 막고있을 뿐다.
왜 필요할까?
동일 출처 정책이 필요한 가장 큰 이유는 바로 보안이다. 2008년 1000만면이 넘는 사용자의 개인정보가 유출된 옥션 해킹사고도 유사한 취약점으로 인해 발생했다고 알려져있다. 그 과정을 재현해보면 다음과 같다.
1. A사이트 관리자가 A사이트에 관리자로 로그인.
2. 해커가 본인이 만든 B사이트로 관리자를 유도하고 관리자는 B사이트에 접속. B사이트에서 A사이트의 링크로 요청이 일어나도록 하여, 관리자 정보 획득 및 조작.
3. B사이트에 접속한것 만으로 A사이트의 정보가 조작됨.
교차 출처 리소스 공유 (Cross Origin Resource Sharing)
원칙적으로 동일 출처 정책에 따르면, 서로 다른 출처간에는 리소스 공유가 불가능해야 한다. 하지만 현실을 보면 그렇지 않다. 위의 사진을 보면 11번가에서 다른 도메인의 이미지를 불러오고, Api도 호출하고 있다. 이는 브라우저가 서로 다른 출처라도 정해진 규칙에 맞다면 자원 공유를 허락해주고 있기 때문이다. 서로 다른 출처간의 자원 공유, 그리고 이를 위한 규칙을 교차 출처 리소스 공유(Cross Origin Resource Sharing)이라고 부른다.
CORS의 동작 방식
CORS의 동작 방식은 단순요청(Simple Request)와 프리플라이트(Preflighted Request)로 나눌 수 있다. 단순요청 방식은 아래의 조건이 모두 충족되어야 한다.
- HTTP Reqeust Method가 GET, HEAD, POST 중 하나여야 한다.
- 브라우저가 자동으로 만드는 Request Header 이외에는 아래의 헤더만 추가할 수 있다.
- Accept
- Accept-Language
- Content-Language
- Content-Type, 값은 application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용
단순 요청방식은 위 이미지처럼, 요청을 그대로 보내고 그 응답을 보고 CORS를 허용할지 판단한다. 위 이미지를 보면 foo.example에서 다른 도메인으로 요청을 보냈고, 응답의 헤더에 Access-Control-Allow-Origin: * 가 있으므로 CORS의 규칙을 만족하여, 브라우져가 요청의 응답을 사용자에게 보여준다.
단순요청 방식의 조건에 충족하지 않는 CORS 요청들은 프리플라이트 방식을 사용한다. 프리플라이트 방식은 본래의 CORS 요청을 보내기 전에 OPTION 메소드를 사용하는 요청을 먼저 보내고 그 응답 헤더를 보고, CORS 요청을 보내도 되는지 판단을 한다.
위 이미지를 보면, foo.example 페이지에서 다른 도메인으로 요청을 보냈다. 요청은 POST 메소드였고, 요청 헤더로 X-PINGOTHER, Content-Type을 가지고 있다. X-PINGOTHER 요청 헤더는 단순요청 방식의 조건에 위반되므로, 이 요청은 프리플라이트 방식을 사용한다. 따라서 브라우저는 원래의 요청이 아니라, 서버의 CORS 정책 확인용 OPTION 메소드 요청을 보낸다. 그리고 서버의 응답 헤더에 Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers를 보고 foo.example에서의 POST 메서드, X-PINGOTHER 요청헤더를 허용한다는 것을 확인하고, 본래의 요청을 다시 보낸다.
CORS 관련 요청/응답 헤더
요청헤더
- Origin: 요청의 출처
- Access-Control-Request-Method: 요청에 사용되는 메소드, 프리플라이트 요청에서 사용된다.
- Access-Control-Request-Headers: 요청에 사용되는 요청 헤더, 프리플라이트 요청에서 사용된다.
응답헤더
- Access-Control-Allow-Origin: CORS를 허용하는 출처
- Access-Control-Expose-Headers: CORS요청의 응답에서 노출할 응답 헤더
- Access-Control-Max-Age: 프리플라이트 결과를 캐시하여 사용할 시간
- Access-Control-Allow-Credentials: 쿠키 사용허용 여부
- Access-Control-Allow-Methods: CORS를 허용하는 메소드
- Access-Control-Allow-Headers: CORS를 허용하는 요청헤더
One More Thing
위의 모든 규칙들을 알고 있어도, CORS 에러를 볼 수 있다. 사실 이 에러메시지가 이 글을 포스팅하게된 이유다. 브라우저는 원칙적으로 동일 출처 정책을 사용하고, 정해진 조건에 따라서 CORS를 허용한다. 하지만 위에 나오지 않은 또 하나의 대원칙이 있다. 브라우저는 보안을 위해서 공인 IP에서 사설 IP로의 낮은 수준으로가는 요청은 허용하지 않는다. IP의 수준은 아래와 같이 나누어진다.
- Local Address: 127.0.0.1, localhost 같은 로컬 주소
- Private Address: 동일 사설망에서 접속 가능한 사설 주소. 공유기에서 각 기기마다 부여되는 사설 IP가 그 예
- Public Adress: 어느 인터넷 환경에서도 접속 가능한 공인 주소
참고 - https://developer.mozilla.org/ko/docs/Web/Security/Same-origin_policy
'STUDY > 기타' 카테고리의 다른 글
Flyway를 활용한 DB Migration (0) | 2023.05.23 |
---|---|
Jest와 React (0) | 2023.02.21 |
CSR 시대의 동적 크롤링 with Selenium (1) | 2022.01.03 |
채팅을 위한 노력의 역사 그리고 WebRTC (1) | 2021.12.02 |
Java 8 ~ 11 버젼별 특징 (0) | 2021.11.01 |