개발 일기

[보안] CORS? (feat. SOP) 본문

Computer Science/보안

[보안] CORS? (feat. SOP)

개발 일기장 주인 2024. 4. 29. 17:55

공부하게된 계기

개발자 유미님의 유투브 강의를 보고 스프링 부트를 통해서 간단한 CRUD기능과 Spring Security를 통해 JWT 토큰 기반 인증을 구현해보고 있었다. 그때 SecurityConfig 클래스 내의 filterChain 메서드에서 CORS (Cross-Origin Resource Sharing) 구성을 설정해보는 것이 있었는데 평소에도 CORS라는게 종종 보이기도 했고 나중에 개발을 하면서 이 설정 파일을 작성하면서 매번 모르고 할 수는 없을 것 같았다. 그래서 정리를 언젠간 꼭 해야되겠다는 생각을 가지고 있었는데 트러스 창업팀에서 프론트에서 테스트로 백엔드에 요청을 보내보는데 프론트엔드는 http://localhost:3000으로 호스팅한 상황이였고 백엔드 API는 http://localhost:8000/~으로 파놓은 상황에서, 브라우저는 CORS 에러가 발생시켰다. 그래서 이것을 해결하고 이것이 왜 발생하는 문제인지 조금 더 자세히 파악할 필요가 있다고 판단하여 이번 기회에  정리해보게 됐다.

 

CORS

CORS란?

CORSCross-Origin Resource Sharing의 약자로 추가 HTTP 헤더를 통해, 어느 한 출처에서 실행 중인 웹 어플리케이션이 특정 다른 출처의 웹 어플리케이션의 자원에 접근할 수 있도록 브라우저에게 알려 권한을 얻는 구조이다. CORS를 직역하면 교차 출처 자원 공유인데 처음에 이 교차라는 말이 있어서 애매했는데 이제 보니 다른(교차) 출처간에 자원을 공유할 수 있도록 한다는 뜻으로 이름을 붙힌 것 같다.

Origin (출처) 란?

위에서 CORS를 다른 출처끼리 리소스 공유를 가능하게 하는 것이라고 했는데 그래서 출처가 다르다는 것이 무엇을 의미하는지 이해할 필요가 있다. 출처는 URL을 기준으로 하며 아래를 참고하자.

위에서 알 수 있듯이 Origin(출처)는 Protocol + Hostname + Port 인 것을 확인 할 수 있다.

그래서 출처가 다른지 비교하려면 위의 3가지 요소를 비교하면 된다. 즉, 프로토콜, 호스트 네임, 포트 번호 중 하나의 요소라도 다르다면 다른 출처로 판단하고 이 세계만 동일하면 뒤에 pathname, search 등이 달라도 같은 출처로 판단하게 된다.

출처를 비교하는 로직은 서버에 구현된 스펙이 아닌 브라우저에 구현된 스펙이다. 따라서 만약 CORS정책을 위반하는 요청에 서버가 정상적으로 응답을 하더라도 브라우저가 이 응답을 분석해서 CORS정책에 위반되면 그 응답은 처리하지 않게 된다.

 

 


브라우저의 개발자 도구 콘솔에서 Location 객체가 가지고 있는 origin 프로퍼티에 접근하여 현재 출처를 알아낼 수도 있다.

Google의 다양한 사이트에서 다음과 같이하면 다음과 같이 출처를 알 수 있음.

SOP란?
CORS는 다른 출처와의 리소스 공유를 허용하는 것이라면 그 반대도 있을 것이다. 바로 그것이 SOP이다.
SOP는 Same-Origin Policy의 약자로 CORS와는 반대로 같은 출처에서만 리소스를 공유할 수 있다는 정책이다.
조금 더 자세히 정리해보면 웹에서는 다른 출처에 있는 리소스를 가져와서 사용하는 일은 굉장히 흔한 일이라 무작정 막을 수 없다. 그래서  몇가지 예외 조항을 두고 이 조항에 해당하는 리소스 요청은 리소스의 출처가 다르더라도 허용하기로 했는데, 그중 하나가 "CORS 정책을 지킨 리소스 요청"인 것이다.
즉, 다른 출처의 리소스를 사용하는 것을 제한하는 행위는 하나의 정책만으로 결정된 사항이 아니라는 의미가 된다.
이런 정책들은 XSS, CSRF공격을 막아 줄 수 있다.

 

CORS의 동작 방식

다른 출처의 리소스를 요청할 때 HTTP 프로토콜을 사용하여 요청을 보내게 되는데, 이때 브라우저는 요청헤더에 Origin이라는 필드에 요청을 보내는 출처를 함께 담아서 보낸다.

// 브라우저에서 요청 보낼때 Origin 헤더에 현재 요청을 보내는 곳의 출처를 담음.
Origin: https://front-host.com

// 서버는 아래 헤더에 허용할 요청의 출처를 지정
Access-Control-Allow-Origin: https://front-host.com

이후 서버가 요청에 대한 응답을 할 때 응답 헤더의 Access-Countrol-Allow-Origin이라는 값에 접근이 허용된 출처를 알려주고 응답을 받은 브라우저는 클라이언트가 보낸 요청의 Origin과 서버가 보낸준 응답의 Access-Countrol-Allow-Origin을 비교하여 이 응답이 유효한 응답인지 아닌지 결정한다. 위의 경우에는 동일하기 때문에 유효한 응답으로 판단한다.

 

기본적인 흐름은 간단하지만 CORS가 동작하는 방식은 한가지가 아닌 세가지 시나리오에 따라 변경된다.

Simple Request

이 시나리오에 대한 정식명칭은 없지만 MDN에서 "단순요청"이라는 용어를 사용하므로 본 포스트에서도 단순요청이라고 부르겠다.

단순요청은 이 다음에 다룰예비 요청(Prefilght)을 보내지 않고 바로 서버에 본 요청을 한 후, 서버가 이에 대한 응답의 헤더에 Access-Control-Allow-Origin과 같은 같을 보내주면 브라우저가 CORS정책 위반여부를 검사하는 방식이다.

Access-Control-Allow-Origin:  * 은 모든 출처를 허용한다는 뜻이다.

단순요청은 특정한 조건을 만족하는 경우에만 성립할 수 있다.

  1. 요청의 메소드는 GET, HEAD, POST 중 하나여야 한다.
  2. 유저 에이전트가 자동으로 설정한 헤더외에, 수동으로 설정할 수 있는 헤더는 Fetch 명세에서 "CORS-safelisted request-header"로 정의한 헤더만 사용할 수 있다.
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  3. Content-Type 을 사용하는 경우에는 다음의 값들만 허용된다.
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

위 조건과 같이 다소 까다로운 조건들이 많기 때문에 위 조건을 모두 만족시키는 상황을 만드는것은 쉽지 않다.

CORS 요청에서 거의 동작하지 않음(중요도 떨어짐)

 

Prefilght Request

프리플라이트(Prefilght) 시나리오에 해당하는 상황일 때 브라우저는 요청을 한번에 보내지 않고 예비 요청과 본 요청으로 나누어서 서버로 전송한다.

이때 브라우저가 예비요청을 보내는 것을 Preflight라고 부르며 예비요청은 OPTIONS메소드를 사용한다. 예비요청의 역할은 본 요청을 보내기전 브라우저가 요청을 보내는 것이 안전한지 확인하는 것이다.

자바스크립트의 fetch API를 통해 브라우저에게 리소스를 받아오게 하면 브라우저는 서버로 예비요청을 먼저 보내고 서버는 이 예비요청에 대한 응답으로 어떤 것을 허용하고 어떤것을 금지하고 있는지에 대한 정보를 담아서 브라우저로 다시 보내준다.

이후 브라우저는 보낸 요청과 서버가 응답해준 정책을 비교하여 해당 요청이 안전한지 확인하고 본 요청을 보내게 된다. 이후 서버가 본 요청에 대한 응답을 하면 최종적으로 이 응답 데이터를 자바스립트로 넘겨준다.

만약 Origin과 Access-Control-Allow-Origin이 동일하지 않다면 cors에러를 발생시킨다.

 

쿠키에 세션 정보가 담기지 않는 HTTP요청은  대부분 이 방식으로 동작한다. 

만약 쿠키에 세션 정보가 담긴 HTTP요청이 발생하는 경우에는 아래의 Credentialed Request로 동작하게 된다.

 

Credentialed Request

마지막 시나리오는 인증된 요청을 사용하는 방법이다. 출처가 다르면 cors에러를 터뜨리고 출처가 같다면 본 요청을 보내는 메커니즘 자체는 Preflight Request와 유사하다. 이 시나리오는 Request Header에 세션 정보가 담긴 쿠키가 담겨오기 때문에 좀 더 보안을 강화하고 싶을 때 사용한다.

  1. Request Header에 세션 정보가 담긴 쿠키가 담겨올 수 있다.
  2. "Access-Control-Allow-Origin: * " 불가. 명시적으로 어떤 출처를 허용할지 URL을 지정해야 한다.
  3. 서버 단에서 응답 헤더에는 반드시 Access-Control-Allow-Credentials:true 설정이 필요하다.

 

 

알게된 점

앞으로 웹 개발을 할때 프론트에서 cors 설정을 해달라고하면 Access-Control-Allow-Origin: 에다가 내가 허용할 출처를 지정해줘야하는 것을 알게 되었고 이러한 이유 때문에 내가 작성한 SecurityConfig에 cors 설정을 해줘야하는 것이였다.