(1) Origin과 SOP
웹 애플리케이션은 HTML, CSS, JavaScript뿐만 아니라 이미지, 폰트, 외부 스크립트 등 다양한 리소스로 구성된다. 이렇게 다양한 웹 리소스들을 이용하기 위해 클라이언트의 요청은 단일 서버로만 향하지 않고, CDN, 외부 API 같은 외부 출처의 서버로도 전달된다.
외부 출처의 리소스를 사용하는 것은 애플리케이션의 컨텐츠를 풍부하게 만들어주고, 개발의 효율성을 높인다. 그러나 동시에 보안 위험을 초래할 가능성도 키운다. 예를 들어, 악의적인 제3자가 사용자의 개인정보를 탈취하는 등 허가되지 않은 내부 리소스에 접근하려 할 수 있다.
이러한 위험을 방어하기 위해 웹 브라우저는 Origin과 SOP(Same-Origin Policy, 동일 출처 정책)라는 기본적인 보안 메커니즘을 구현하고 있다.
1) Origin
Origin은 웹 보안의 기본 단위로 웹 리소스의 출처를 의미한다.
프로토콜(scheme)+호스트(host)+포트(port)
의 조합으로 표현되며, URL의 구조 scheme://host[:port]
[/path][?query]
에서 scheme://host[:port]
에 해당한다. 아래 예시에서 밑줄친 부분이 Origin이다.https://www.google.com/
search?q=cat
http://localhost:3000/
page1
웹 브라우저는 Origin을 기준으로 애플리케이션에 외부 리소스가 접근하는 것을 제어한다. 또한 웹 서버는 Origin 정보에서 요청의 출처를 파악해 내부 리소스 접근을 제어한다.
2) SOP(동일 출처 정책)
앞서 설명한 대로 외부 리소스가 애플리케이션에 접근하는 것은 보안 위험을 수반한다. 이를 방지하기 위해 브라우저에서 사용하는 기본적인 보안 정책이 SOP이다.
SOP(Same-Origin Policy, 동일 출처 정책)는 같은 Origin의 리소스 접근만을 허용하는 보안 정책이다. 외부 리소스를 요청하고 응답을 받을 때, 브라우저는 해당 응답의 출처와 현재 웹 페이지의 출처를 비교한다. 그리고 출처가 다른 리소스의 접근을 차단함으로써 웹 애플리케이션의 보안을 유지한다.
동일 출처 판단:
http://example.com/dir/page.html
에서 요청을 보낼 때
http://example.com/dir/page2.html
(허용)
http://example.com/dir2/page.html
(허용)
https
://example.com/dir/page.html
(차단, 프로토콜 불일치)
http://example.com
:81
/dir/page.html
(차단, 포트 불일치)
http://
news
.example.com/dir/page.html
(차단, 호스트 불일치)
SOP의 적용 여부는 요청의 종류에 따라 결정된다. 대체로 HTML 태그로 다른 출처의 리소스를 불러오는 것은 허용이 되나, JavaScript로 이러한 리소스의 내용을 읽거나 조작하려고 할 때 SOP가 작동한다.
SOP 작동 사례
- HTML 태그를 통한 외부 리소스 요청: SOP 제한 없음
- e.g.,
<img src="https://other-site.com/image.jpg">
<img>
, <link>
, <script>
, <iframe>
, <video>
, <audio>
, <object>
, <embed>
등- 외부 리소스를 JavaScript로 조작: SOP 제한 적용
- e.g.,
iframe.contentDocument.body.innerHTML
(차단)
<iframe src="https://other-site.com">
내부의 DOM 접근 불가- JavaScript를 이용한 비동기 통신: SOP 제한 적용
- e.g.,
fetch('https://other-site.com/api')
(차단)
XMLHttpRequest, Fetch API를 통한 요청의 응답 제한
- 웹 폰트: 대체로 SOP 제한 적용
- e.g.,
@font-face { src: url("https://other-site.com/font.woff2") ... }
(허용)
@font-face
규칙을 통해 로드되는 외부 폰트 파일은 예외적으로 SOP 제한 없음SOP 제한이 적용되는 경우, 브라우저는 서버 응답에 포함된
Access-Control-Allow-Origin
헤더를 확인한다. 그리고 요청을 보낸 출처가 헤더에 포함되어 있는지에 따라 리소스의 접근을 결정한다. 이렇게 브라우저는 SOP를 구현하여 신뢰할 수 없는 외부 출처로부터의 무단 접근을 차단하고, 사용자 개인정보 유출, 잠재적인 공격 위험으로부터 애플리케이션을 보호할 수 있다.
(2) CORS
1) 이해
SOP의 엄격한 규칙은 웹 애플리케이션의 보안을 강화하지만, 동시에 정상적인 외부 출처의 리소스마저 차단하는 문제를 야기한다. 현대 웹 개발에서는 풍부한 컨텐츠와 기능 확장을 위해 외부 리소스 임베드, 외부 API 사용 등의 기능이 사용된다. 이 때 현재 웹 페이지와 다른 출처, 즉 크로스 오리진을 통한 통신이 필수적이므로 이를 위해서 SOP 제한 문제를 해결할 필요가 있다.
CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)는 추가 HTTP 헤더를 사용해 크로스 오리진(교차 출처)의 접근을 허용할 수 있게 해주는 보안 메커니즘이다. 서버가 HTTP 응답 헤더에 접근 허용 조건을 명시하면, 이를 만족하는 요청에는 SOP 제한이 적용되지 않는다. 이러한 조건을 설정하는 헤더를 CORS 헤더라고 하며, 이를 통해 클라이언트는 외부 리소스에 안전하게 접근할 수 있다.
2) CORS 헤더
서버는 CORS 헤더로 어떤 출처와 어떤 HTTP 메서드의 요청을 허용할지 등을 지정할 수 있다. 아래는 Express¹⁾ 프레임워크로 만든 서버에 CORS 헤더를 설정하는 간단한 예시 코드이다.
응답 헤더에 추가할 수 있는 CORS 관련 설정은 다음과 같다.
Access-Control-Allow-Origin
: 허용할 출처를 지정. 하나만 지정할 수 있으며, URL 대신*
(와일드카드)로 표기하면 모든 출처의 접근을 허가할 수 있음
Access-Control-Allow-Methods
: 허용할 HTTP 메서드 지정
Access-Control-Allow-Headers
: 허용할 HTTP 헤더 지정
Access-Control-Allow-Credentials
: 쿠키, HTTP 인증 등 자격 증명를 포함한 요청 허용 여부 지정
Access-Control-Expose-Headers
: 브라우저가 접근할 수 있는 커스텀 헤더 지정
Access-Control-Max-Age
: 사전 요청²⁾ 결과를 캐시할 시간 지정
이 중
Access-Control-Allow-Origin
헤더는 허용되지 않은 출처의 요청에 대한 서버 응답을 브라우저가 차단하는 데 사용되므로, 클라이언트 측 보안 강화에 특히 중요하다.3) 작동 방식
CORS 헤더 설정은 클라이언트의 리소스 접근을 제한하지만, 서버로 전송되는 요청 자체를 막지는 못한다. 예를 들어 공격자가 사용자의 인증 정보를 탈취해 서버에 DELETE 요청을 보내는 경우, 서버는 요청의 출처를 확인하지 않고 사용자의 정보를 삭제할 수 있다. 이렇게 허용되지 않은 출처로부터의 요청이 서버의 데이터를 수정하거나 삭제하는 것을 방지하기 위해, CORS는 단순 요청과 위험 가능성이 있는 요청을 구분하여 처리한다.
① 단순 요청
기존의 데이터를 수정/삭제하지 않는 요청이다. 구체적으로는 아래 조건을 모두 만족해야 한다. 단순 요청 처리에서 브라우저는 서버에 바로 요청을 보내고 서버의 응답 헤더를 확인하여 CORS 정책을 검사한다.
단순 요청의 조건: 모두 만족하지 않으면 사전 요청 처리가 됨
- 메서드: GET, HEAD, POST 중 하나를 사용
- 헤더: 자동 설정 헤더와
Accept
,Accept-Language
,Content-Language
,Content-Type
외의 커스텀 헤더를 사용하지 않는 경우
Content-Type
헤더 사용시: 그 값이application/x-www-form-urlencoded
,multipart/form-data
,text/plain
중 하나인 경우
② 사전 요청(Preflight 요청)
기존의 데이터를 수정/삭제할 가능성이 있는 요청으로, 단순 요청 조건을 만족하지 않으면 모두 사전 요청으로 처리된다. 사전 요청 처리에서는 본 요청 전에 브라우저가 OPTIONS 메서드를 사용해 사전 요청(Preflight)을 보낸다. 서버는 허용되는 메서드, 헤더 등 CORS 정책 정보를 응답하고, 브라우저는 본 요청이 허용될지 판단한 뒤, 그 여부에 따라 본 요청을 보내 응답값을 처리한다.
사전 요청은 본 요청 전에 서버의 CORS 정책을 확인함으로써 불필요한 요청을 방지하고 추가적인 보안 계층을 제공한다. 이 과정은 브라우저가 자동으로 처리하며 개발자가 직접 구현할 필요는 없다.
![[그림 2-6] 사전 요청(Preflight)](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F6d7fa230-23c2-4b9c-b287-fdeb076e90bd%2F07ef202f-b3cd-4649-8528-de326e92af00%2Fimage_22.png?table=block&id=a00bfeaf-ccff-4b9e-b796-ceec54611ec0&cache=v2)
4) 실습
① CORS 에러 이해
CORS 에러가 발생하면 콘솔에서 아래와 같은 에러 메시지를 확인할 수 있다.
![[그림 2-7] 악명높은 CORS 에러](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F6d7fa230-23c2-4b9c-b287-fdeb076e90bd%2F248f3dbf-1487-4603-9da2-ee6964926f25%2Fimage_13.png?table=block&id=82e31686-8f49-41a1-bdc6-6942f75dc18d&cache=v2)
에러 메시지 이해
- 요청 출처:
http://127.0.0.1:5500
- 응답 출처:
https://www.google.com/
- CORS 에러 발생 이유: 서버 응답에
Access-Control-Allow-Origin
헤더가 없음
에러의 원인은 메시지 내에서 확인이 가능하다. 주로 서버 측 설정을 변경해야 해결이 가능하므로 프론트엔드 개발자는 백엔드 개발자와 협력하여 적절한 CORS 설정을 구현해야 한다.
② 클라이언트 측 CORS 설정
CORS 에러 해결이 주로 서버 측 설정에 의존하는 이유는 서버의 기본 설정이 CORS를 거부하는 것이기 때문이다. 반면 클라이언트 측의 기본 설정은 브라우저가 CORS를 사용하도록 되어 있다. 그리고 개발자는 일부 상황에서 이를 제어할 수도 있다.
- Fetch API의 CORS 요청 모드
Fetch API를 사용하면
mode
옵션으로 CORS의 요청 모드를 변경할 수 있다.요청 모드 | 의미 |
cors(기본값) | CORS 사용. 서버의 CORS 설정에 따라 요청이 허용되거나 거부됨 |
same-origin | CORS 미사용. 동일 출처 요청만 허용하고, 다른 출처로의 요청은 에러 발생 |
no-cors | CORS 미사용. 교차 출처 요청 중 단순 요청만 허용하고, 불투명한 응답³⁾을 받음 |
- HTML 요소의 crossorigin 속성
HTML 요소에서 전송되는 요청은 기본적으로 CORS를 사용하지 않지만,
crossorigin
속성을 추가하면 CORS 요청을 활성화할 수 있다.crossorigin | 의미 |
속성 없음(기본값) | CORS 미사용. 교차 출처 요청 중 단순 요청만 허용하고, 불투명한 응답³⁾을 받음 |
anonymous | CORS 사용. 쿠키, HTTP 인증 등 자격 증명 없는 CORS 요청 |
use-credentials | CORS 사용. 자격 증명을 포함한 CORS 요청. CORS 헤더에 특정 URL 지정 필요 |
③ 쿠키를 포함하는 요청의 CORS 설정
HTTP의 무상태성으로 인해 웹 애플리케이션은 상태 정보 유지시 쿠키를 사용한다. 그런데 브라우저는 보안상 기본적으로 교차 출처 요청에 쿠키를 포함시키지 않는다. 쿠키를 포함한 교차 출처 요청을 허용하려면 클라이언트와 서버 양측 모두에 추가 설정이 필요하다.
- 클라이언트 측 설정
credentials
은 쿠키, HTTP 인증 등 자격 증명을 의미한다. 클라이언트 측에서는 통신에 자격 증명을 포함할 것을 직접 설정해주어야 한다. XMLHttpRequest, Axios 사용시엔 이를 불린 값으로 표현하지만, Fetch API에서는 세가지 옵션이 주어진다. credentials | 의미 |
omit | 쿠키 전송 안 함 |
same-origin(기본값) | 동일 출처 요청에만 쿠키 포함, 다른 출처로 쿠키 전송 방지 |
include | 모든 요청에 쿠키 포함 |
- 서버 측 설정
Access-Control-Allow-Credentials
:true
값으로 쿠키를 포함할 것을 설정Access-Control-Allow-Origin
: 허용할 출처로 특정 URL 지정.*
(와일드카드) 사용 불가
④ Proxy 서버를 이용한 요청 우회
마지막으로 서버 측 코드를 변경하지 않고도 CORS 에러를 피할 수 있는 방법을 소개한다. 바로 클라이언트와 서버의 중개 서버인 proxy 서버(이하, 프록시)를 이용하는 방법이다.
![[그림 2-8] 프록시 중개 서버](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F6d7fa230-23c2-4b9c-b287-fdeb076e90bd%2F4c4e8c57-1678-4b68-abaf-3e8020bd77e3%2FFrame_2.png?table=block&id=fa59b8d4-002a-449d-98b4-ec428d452189&cache=v2)
이 방법에서 프록시는 웹 서버와 애플리케이션 서버 사이에 위치한다. [그림 2-8]에서는 웹 서버가 프록시 작업을 병행하고 있다.
프록시 서버는 클라이언트와 동일 출처를 사용하므로 클라이언트의 요청을 받을 때 CORS 에러가 발생하지 않는다. 그리고 이 요청을 우회해서 교차 출처의 애플리케이션 서버로 전달하는데, 이 때엔 브라우저를 거치지 않는 서버간의 통신이므로 역시나 CORS 제한이 적용되지 않는다.
프록시 우회 시나리오
https://example.com
사이트에서 'user'라는 사용자의 프로필 정보가 필요함
- 브라우저는
https://example.com/api/@user
주소로 서버에 요청을 보냄
- 2번은 동일 출처 요청이기 때문에 CORS 에러가 발생하지 않음
- 웹 서버에서
/api
경로로 들어온 요청은 프록시 설정에 의해 애플리케이션 서버https://cross-origin.com/@user
로 우회하여 전달됨
- 4번은 서버 간 통신이므로 브라우저의 SOP 영향을 받지 않아 CORS 제한 없음
- 애플리케이션 서버가 응답하면 프록시가 이를 원 요청자인 클라이언트로 전달함
¹⁾ Express는 Node.js로 웹 서버를 쉽게 개발할 수 있도록 해주는 대표적인 프레임워크이다. 이 책은 서버 측 코드를 최소한으로만 다루며 Express를 몰라도 이해할 수 있도록 서술되었다.
³⁾ opaque response. 보안 목적에 의해 리소스 자체는 실행되고 화면에 보이지만 JavaScript로 접근할 수 없는 리소스. 예를 들어, 리소스가 이미지일 경우 화면에 표시는 되지만 JavaScript로 픽셀 데이터에 접근 불가함