SOP와 CORS
XSS나 CSRF 등의 보안 취약점을 노린 공격을 방어하기 위한 정책인 SOP와 그를 허용하기 위한 CORS에 대해 알아봅니다.
SOP(Same-Origin-Policy)
SOP란 같은 출처의 리소스만 공유할 수 있다는 정책입니다. 하지만 다른 출처의 리소스를 사용하는 일은 굉장히 흔한 일이라 몇가지 예외 사항을 두고 있는데, 그 중 하나가 CORS 정책을 지킨 리소스 요청입니다.
즉, 브라우저가 SOP를 지키기 때문에 CORS 정책을 지켜 개발을 해야 하는 것입니다. 그렇다면 어차피 정해진 서버로만 통신을 할 텐데 왜 이런 정책이 필요한지 알아보겠습니다.
SOP의 과정과 해결 방법
SOP는 서버가 아닌 브라우저에 구현되어 있는 로직입니다. SOP 정책을 위반하더라도 API 테스트 과정에서는 서버가 정상적으로 응답을 하게 되지만, 브라우저를 통해 요청할 때는 서버가 정상적으로 응답하더라도 브라우저가 그 응답을 버리게 됩니다.
이를 통해 브라우저는 XSS나 CSRF 등의 공격 방식을 방어할 수 있습니다. 하지만 다른 출처의 리소스를 사용하는 것은 아주 흔한 일이기 때문에 CORS 정책을 지킨 리소스 요청을 허용하는 예외를 두고 있습니다.
CORS(Cross-Origin-Resource-Sharing)
CORS는 브라우저에서 다른 출처의 리소스를 공유하는 방법입니다.
먼저 출처를 알기 위해서는 URL 구조에 대해 알고 있어야 합니다.
https://jjmin321.github.io:443/development/CORS/
https - Protocol
jjmin321 - Host
github.io - Domain
:443 - Port (생략됨)
/development/CORS - Path
위에서 나온 Protocol, Host, Domain, Port를 합친, 즉 https://jjmin321.github.io를 출처라고 합니다.
CORS 동작 원리
기본적으로 웹 애플리케이션이 리소스를 요청할 때는 HTTP 프로토콜을 통해 요청을 보내게 되는데, 이 때 브라우저는 요청 헤더의 Origin이라는 필드에 출처를 함께 담아보냅니다.
이후 서버는 응답을 할 때, Access-Control-Allow-Origin이라는 값에 요청이 허용된 출처를 보내주고 브라우저는 요청한 출처가 허용된 출처인지를 확인합니다.
response.setHeader("Access-Control-Allow-Origin", "jjmin321.github.io");
기본적으로는 간단하지만, CORS의 동작 방식은 한 가지가 아닌 세 가지의 시나리오에 따라 변경되기 때문에 어떤 시나리오에 위반되었는지 알기 위해 모든 시나리오에 대해 알고 있어야 합니다.
Preflight Request
Preflight Request는 일반적으로 우리가 웹 애플레케이션을 개발할 때 가장 많이 마주치는 시나리오입니다. 이 시나리오에 해당하는 상황일 때 브라우저는 요청을 한 번에 보내지 않고 예비 요청과 본 요청으로 나누어서 서버를 전송합니다.
브라우저가 본 요청을 보내기 전에 보내는 예비 요청을 Preflight라고 하며, 이 예비 요청에는 HTTP 메소드 중 OPTIONS 메소드가 사용됩니다. 예비 요청의 역할은 본 요청을 보내기 전에 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인하는 것입니다.
이 과정을 플로우 차트로 나타내면 이와 같습니다.
자바스크립트를 사용해 브라우저에게 리소스를 받아오라는 명령을 내리면 브라우저는 서버에게 OPTIONS 메소드를 통해 예비 요청을 먼저 보내고, 서버는 이 예비 요청에 대한 응답으로 어떤 것들을 허용하고, 어떤 것들을 금지하고 있는지에 대한 정보를 응답 헤더에 담아서 브라우저에게 보내줍니다.
이후 브라우저는 서버의 CORS 정책을 비교한 후, 안전하다고 판단하면 본 요청을 실제 메소드로 보내게 되고 본 요청에 대한 응답을 실제 자바스크립트에게 넘겨줍니다.
대부분의 경우 이렇게 예비 요청과 본 요청을 나누어 보내는 Preflight Request 를 사용하지만, 특정 조건을 만족하는 경우에는 Simple Request를 통해 검사를 합니다.
Simple Request
Simple Request는 Preflight Request와 달리 에비 요청을 보내지 않고 바로 서버에 본 요청을 보낸 후 서버가 어떤 것들을 허용하고, 어떤 것들을 금지하고 있는지에 대한 정보를 응답 헤더를 통해 받아 즉시 CORS 정책 위반 여부를 검사하는 방식입니다.
즉, Prefligh Request 방식과 로직은 같지만, 예비 요청의 존재 유무만 다릅니다.
Simple Request의 경우는 특정 조건을 만족하는 경우에만 사용되는데 그 특정 조건은 아래와 같습니다.
- 요청의 메소드로 GET, HEAD, POST 중 하나를 사용한다.
- Accept, Content-Type 등 특정 헤더만을 사용해야 한다.
- Content-Type을 사용하는 경우 application/x-www-form-urlencoded, multipart/form-data, text/plain 만을 사용한다.
Credentialed Request
Credentialed Request는 인증된 요청을 사용하는 방법입니다. 이 시나리오는 CORS의 기본적인 방식이라기보다는 다른 출처 간 통신에서 좀 더 보안을 강화하고 싶을 때 사용하는 방법입니다.
credentials 옵션을 추가하면 인증과 관련된 정보를 추가로 담을 수 있으며 브라우저가 다른 출처의 리소스를 요청할 때 단순히 Access-Control-Allow-Origin만 확인하는 것이 아닌 추가적인 검사 조건을 통해 검사하게 됩니다.
response.setHeader("Access-Control-Allow-Origin", "*");
기본적으로 서버에서 위와 같이 모든 출처를 허용한다면 브라우저는 이 요청이 안전하다고 판단하게 됩니다. 하지만 credentials 옵션을 사용하면 다릅니다.
예를 들어 axios로 토큰 갱신 API를 사용할 때, 보통 클라이언트와 서버는 다른 출처이기 때문에 요청 헤더에 쿠키가 자동으로 추가되지 않아 이처럼 쿼리스트링을 사용해 API를 사용합니다.
axios.get(`${SERVER_ADDRESS}/user/token?refreshToken=${refreshToken}`);
하지만 withCredentials 설정을 통해 브라우저의 쿠키 정보가 자동으로 추가되어 URI가 아닌 URL만으로 리소스를 표현할 수 있습니다.
axios.get(`${SERVER_ADDRESS}/user/token`, { withCredentials: true });
또한 이를 사용하기 위해서는 추가로 서버에서 Credential 설정을 true로 해줘야 합니다.
response.setHeader("Access-Control-Allow-Credentials", "true");
이처럼 credentials 옵션을 사용하여 요청에 현재 브라우저의 정보를 추가하면 브라우저는 CORS 정책 위반 검사에 두 가지 규칙을 추가하게 됩니다.
- Access-Control-Allow-Origin에 *를 사용할 수 없음
- 서버에서 반드시 Access-Control-Allow-Credentials 값을 true로 줘야 함
Leave a comment