CORS(Cross-Origin Resource Sharing)
교차 출처 리소스 공유라는 뜻이다.
한 출처에 있는 자원에서 다른 출처에 있는 자원에 접근하도록 한다.
출처(Origin)
동일 출처란 프로토콜 + host + port가 같은 것이다.
path가 다른 것은 상관이 없다.
동일 출처
모두 같은 출처이다.
다른 출처
기준 : http://m1.com
- 프로토콜이 다르다 : https://m1.com
- host가 다르다 : http://www.m1.com
- 포트가 다르다 : http://m1.com:8080
다른 출처 요청의 위험성
<img>, <script>, <frame> 등이 웹에 등장하면서, 페이지 로딩 이후에 다른 출처로부터의 요청을 가져올 수 있게 되었다.
예를 들어, 악성 페이지 접속시 script가 실행되어 은행 사이트에 출금하는 요청을 생각해보자.
사용자가 은행 사이트 https://mybank.com에 로그인해있다고 가정하자.
그 상태에서 악성 사이트인 https://badguy.com에 접속했다.
이 악성 사이트에는 아래와 같은 JavaScript가 포함되어 있을 수 있다.
fetch('<https://mybank.com/api/transfer>', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 1000,
toAccount: '123-456-789'
}),
});
스크립트가 실행되어 악성 사이트인 https://badguy.com에서 은행 사이트인 https://mybank.com으로 요청을 보낸다.
이로 인해, 사용자가 의도하지 않게 송금을 시도하게 된다.
브라우저의 CORS 정책
실제로 브라우저는 기본적으로 다른 출처로부터의 요청을 허용하지 않는다. 👍
즉, 악성 사이트에서 시작된 요청이 은행 사이트에 요청하는 것을 허용하지 않는다.
악성 웹사이트 접근시 아래와 같은 과정으로 동작한다.
- 사용자가 악성 웹사이트 접속 : 사용자는 단순히 웹사이트를 방문한 상태이고, CORS 정책이 직접적으로 적용되지 않는다.
- 악성 웹사이트가 다른 출처(은행 사이트)로의 요청을 보냄 : 사용자가 악성 웹사이트 접속 후, 악성 스크립트가 실행되어 사용자를 대신해 다른 출처의 리소스(은행 출금 API)에 요청을 보낸다.
이때 요청은 사용자의 브라우저를 통해 이루어지므로, 요청이 보내진 출처(악성 웹사이트)와 요청의 목적지(은행 사이트)가 다른 출처인지 브라우저가 판단한다.
브라우저는 다른 출처인지 확인하고, 은행 사이트로부터 온 응답 헤더에 Access-Control-Allow-Origin 값을 확인한다.
해당 헤더에 요청을 보낸 출처(악성 웹사이트)가 존재하지 않는다면 브라우저는 CORS 정책 위반으로 판단하고, CORS 에러를 발생시킨다.
은행 서버는 은행의 프론트엔드 서버를 Access-Control-Allow-Origin에 등록해두어야 CORS 에러가 발생하지 않는다.
Access-Control-Allow-Origin: https://bankfrontend.com
즉, CORS 설정은 서버에서 구현되고, 서버 문제가 아니라면 클라이언트 부분을 살펴보아야한다.
CORS 정책의 허점과 해결방법
CORS 정책에 의해 브라우저가 리소스에 대한 응답을 반환하지 않더라도, 요청 자체가 서버에 도달하지 않았다는 것은 아니다.
즉, 클라이언트 측에서 요청에 대한 응답을 읽거나 이용하는 것을 제한하는 것이지 서버에 요청이 도달하는 것 자체를 막을 수는 없다.
위 예시에서도 출금 요청은 CORS 정책에 의해 막을 수 없다. 오로지 응답을 읽는 것만 막을 수 있다.
이러한 이유로 서버 측에서는 CORS 정책 뿐만 아니라 다른 보안 조치들(인증 토큰, CSRF 토큰, API Key, 인증 헤더 등)을 함께 사용하여 악성 요청을 방지해야한다.
또한 현대 브라우저에서는 복잡한 요청의 경우 Preflight Request(예비 요청)이 적용되는데, 이러한 허점을 어느정도 해결할 수 있다.
(단순 요청의 경우 예비 요청이 적용되지 않는다.)
Preflight Request(예비 요청)
브라우저(ex) 크롬, 사파리)에 의해 결정되는 정책으로, 웹 표준을 따르는 모든 현대 브라우저에서 작동으로 적용된다.
브라우저는 예비 요청을 보내 서버와 잘 통신되는지 확인한 후 본 요청을 보낸다.
예비 요청을 통해 브라우저 스스로 자신이 보내는 요청이 안전한 요청인지 미리 확인한다.
예비 요청 실패시 본 요청은 서버로 전송되지 않고, 브라우저 단계에서 차단된다.
위 메커니즘은 언급했던 허점등의 이유로 보안 목적으로 설계되었다.
다른 출처에서 온 요청이 사용자의 데이터에 무단으로 접근하거나 변경하는 것을 방지하기 위해, 브라우저는 예비 요청을 사용하여 서버의 허용 정책을 사전에 확인한다.
예비 요청은 브라우저가 판단하기에 복잡한 요청의 경우에만 실행된다.
복잡한 요청
- PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH 등의 HTTP 메소드를 사용하는 요청
- 사용자 정의 헤더를 포함하는 요청
- application/json과 같은 단순 요청에서 허용되지 않는 Content-Type을 사용하는 요청
단순한 요청
- 요청의 메소드는 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중 하나여야한다. 아닐 경우 예비 요청으로 동작된다.
다행히 단순한 요청의 조건이 까다롭기 때문에 대부분의 API 요청은 예비 요청으로 이루어진다. 😁
결과적으로 안전한 현대 브라우저(크롬 등)으로 악성 웹사이트에 접속시 post 요청으로 보낸 은행 출금 요청은 예비 요청에서 실패하여 전송되지 않는다.
'문제&해결' 카테고리의 다른 글
Spring WebSocket 애플리케이션에 Spring Security 적용하기(simpUser, Interceptor, handler) (2) | 2024.05.15 |
---|---|
[Test] @Mock, @InjectMocks 동작 원리 및 주의 사항 (0) | 2024.04.10 |
[Test] 비즈니스 로직 정확도 향상 전략 및 공통 테스트 유틸리티 클래스 활용 (0) | 2024.03.19 |
Builder 패턴 객체 생성시 필드 초기값 무시되는 문제 (0) | 2024.03.17 |
[Test] 데이터 의존성을 줄인 통합테스트 코드 작성 (MockUser, Spring Security) (0) | 2024.03.17 |