만자의 개발일지

[WEB] 동일 출처 정책(SOP)과 교차 출처 리소스 공유(CORS)란? 본문

IT/WEB

[WEB] 동일 출처 정책(SOP)과 교차 출처 리소스 공유(CORS)란?

박만자 2022. 2. 22. 15:50

웹 개발을 해본 분이라면 한번쯤은 CORS 에러를 접해보셨을 건데요, 보통 API를 호출하는 과정에서 이 CORS라는 녀석이 골치아프게 한적이 있으실 겁니다. 이번 포스팅에서는 CORS가 무엇이고 어떻게 동작하는지와 CORS의 반대개념인 동일 출처 정책(Same Origin Policy, SOP)에 대해서도 간략히 짚고 넘어가도록 하겠습니다.

 

동일 출처 정책(Same Origin Policy, SOP)

브라우저는 기본적으로 내 서버가 아닌 다른 서버에서 받아온 데이터는 차단합니다. 브라우저가 사용자가 방문하는 사이트를 신뢰하지 않기 때문이죠. (여기서의 서버는 URL(ex https://www.manja.com) 을 의미합니다.)

그 이유는 기본적으로 브라우저는 토큰이나 쿠키 등과 같이 사용자의 정보와 관련된 데이터를 저장하는데, 만약 해커가 이를 탈취해(CSRF, XSS) 인증 요청에 이를 실어보내고 그로인해 얻은 정보를 해커의 서버로 보내버린다면 이는 아주 심각한 문제가 될 것입니다. 이를 막기 위해 등장한 것이 바로 동일 출처 정책(SOP)입니다.
이제부터 SOP라고 부르도록 하겠습니다.

SOP는 말 그대로 자신과 동일한 도메인만 서버로부터 데이터를 요청하여 받을 수 있도록 하는 정책입니다.

예를 들어 https://www.manja.com URL로 서버에 요청을 보내면 서버는 https://www.manja.com URL로만 응답을 보낼 수 있습니다.

이처럼 SOP는 동일한 출처(Origin, URL)로만 응답을 허용합니다. 브라우저는 강제적으로 SOP를 기본으로 합니다.

그렇다면 어떤 것이 동일 출처이고 어떤 것이 교차 출처일까요?

 

동일 출처와 교차 출처

아래 URL을 기준으로 동일 출처와 교차 출처를 비교해 보도록 하겠습니다.

https://www.manja.com
URL 출처
https://www.manja.com/about 동일 출처
https://www.manja.com/about?q=query 동일 출처
https://abc.manja.com 교차 출처
http://www.manja.com 교차 출처
https://www.manja.com:8080 교차 출처

동일 출처와 교차 출처의 기준은 Protocol, Host(도메인), Port 동일 여부입니다. 이중 하나라도 Origin과 다르다면 그 URL은 교차 출처로 인식 됩니다.

이러한 SOP의 빡빡한 정책 때문에 외부에서 데이터를 불러오진 못하지만, CSRF나 XSS 같은 보안 취약점 공격으로부터 안전하다는 장점이 있습니다.

SOP에 대해 어느정도 알아보았으니, 이제 이 SOP를 풀어주는 역할인 CORS에 대해 알아보도록 합시다.

 

교차 출처 리소스 공유(Cross Origin Resource Sharing, CORS)

 

SOP가 동일한 출처간에만 요청과 응답을 허용하는 정책이였다면 CORS는 그와 반대로 서로다른 출처간에도 요청과 응답을 허용하는 정책입니다. Origin이 달라도 서버에서 CORS옵션을 허용해주면 다른 Origin으로 응답을 보낼 수 있게 됩니다.

이러한 CORS의 요청방식에는 크게 두 가지 방식이 있습니다. 바로 Simple RequestPreflight Request입니다.
각각이 무엇이고 두 가지 방식의 차이점에 대해 알아보도록 하겠습니다.

 

Simple Request

https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

먼저 다음과 같은 조건을 만족하면, 브라우저는 해당 CORS 요청을 Simple Request로 처리합니다.

  • HTTP Method가 GET, POST, HEAD 중 하나인 경우
  • Content-Type 헤더가 다음 중 하나인 경우
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • CORS-safelisted request-header를 포함하는 경우(Fetch spec)
  • XMLHttpRequest.upload 에 이벤트 핸들러, 리스너가 등록되지 않은 경우
  • ReadableStream 객체가 포함되지 않은 경우

Simple Request의 경우 다음과 같은 방식으로 동작합니다.

  1. 사용자가 요청 헤더에 자신의 Origin을 실어서 서버로 요청을 보낸다.
  2. 서버는 요청 헤더의 Origin을 확인한다.
  3. CORS 요청이 유효하다면 서버는 응답 헤더에 Accecss-Control-Allow-Origin 헤더를 추가해 사용자에게 다시 전송한다.


예를 들어 다음과 같이 서버에 요청을 보냈다고 가정해 봅시다.

GET /about HTTP/1.1

Origin: https://www.manja.com
.
.
.


아래와 같이 서버의 응답에 Access-Control-Allow-Origin 헤더가 존재하지 않다면 브라우저는 해당 응답을 허용하지 않고 CORS 에러를 반환합니다.

HTTP/1.1 200 OK
.
.
.


브라우저가 해당 응답을 허용할려면 다음과같이 Access-Control-Allow-Origin 헤더가 정의되어있어야 합니다.

HTTP/1.1 200 OK

Access-Control-Allow-Origin: *
.
.
.


이처럼 브라우저는 서버에 CORS 요청을 보내면 응답 헤더에 Access-Control-Allow-Origin 헤더를 보고 응답의 허용여부를 결정합니다.

그렇다면 Access-Control-Allow-Origin은 무엇일까요?

일단 Access-Control-Allow-Origin은 응답 헤더 리스트중 하나입니다. Access-Control-Allow-Origin을 * 로 설정하면 브라우저는 credentials 옵션이 없는 요청에 한해 모든 Origin이 해당 리소스에 접근 가능하도록 허용해줍니다.

Access-Control-Allow-Origin을 https://www.manja.com 으로 지정하면 브라우저는 해당 Origin에서만 리소스를 접근 가능하도록 허용합니다.

이외에도 여러가지 HTTP 헤더가 있는데요, 하나씩 살펴보도록 하겠습니다.

요청 헤더

  • Origin
    • 현재 자신의 URL 정보를 포함합니다.
  • Access-Control-Request-Method
    • Preflight Request에서 사용되며 어떤 Method를 사용할지 서버에게 알리기 위해 사용되는 헤더입니다.
    • POST, GET, DELETE 등이 포함됩니다.
  • Access-Control-Request-Headers
    • Preflight Request에서 사용되며 어떤 Header를 사용할 것인지 서버에게 알리기 위해 사용되는 헤더입니다.
    • X-PINGOTHER, Content-Type 등이 포함됩니다.

Access-Control-Request-* 헤더의 경우 실제 POST 요청시에는 포함되지 않고 OPTIONS 요청시에만 사용됩니다.

응답 헤더

  • Access-Control-Allow-Origin
    • 브라우저가 해당 Origin이 리소스에 접근 가능하도록 허용할 때 사용되는 헤더입니다.
    • * 로 설정할 경우 브라우저는 credentials 옵션이 없는 요청에 한해 모든 Origin이 해당 리소스에 접근 가능하도록 허용합니다.
  • Access-Control-Allow-Methods
    • Preflight Request에 대해 리소스에 접근할 때 허용되는 Method를 지정하기 위해 사용되는 헤더입니다.
    • POST, GET, OPTIONS, * 등이 포함됩니다.
  • Access-Control-Allow-Headers
    • Preflight Request에 대해 해당 요청에서 사용할 수 있는 Header를 지정하기 위해 사용되는 헤더입니다.
    • X-PINGOTHER, Content-Type 등이 포함됩니다.
  • Access-Control-Allow-Credentials
    • Credentialed Request 방식이 사용될 수 있는지를 지정하기 위해 사용되는 헤더입니다.
    • true | false 만 포함됩니다.
    • 예를 들어 Simple Request에 withCredentials: true가 지정되어 있는데, 응답 헤더에 해당 헤더가 true로 명시되어 있지 않다면, 해당 응답은 브라우저에 의해 무시됩니다.
    • Preflight Request에 대해 해당 헤더가 false로 명시되어 있다면, 해당 요청은 Credentialed Request를 보낼 수 없습니다.
  • Access-Control-Max-Age
    • Preflight Request에 대해 캐쉬에 얼마나 오랫동안 남아있는지를 지정하기 위해 사용되는 헤더입니다.
    • 단위는 초(Second)입니다. 
  • Access-Control-Expose-Headers
    • 브라우저 측에서 접근할 수 있게 허용해주는 헤더를 지정하기 위해 사용하는 헤더입니다.
    • 기본적으로 브라우저에게 노출되지 않습니다.
    • Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma 등이 포함됩니다.


이처럼 HTTP 헤더에 대해 알아봤는데요, 설명에 보면 Preflight Request가 자주 보일 겁니다. 이제 이 Preflight Request가 무엇인지 알아봅시다.

 

Preflight Request

https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

Preflight Request는 Simple Request의 조건을 만족하지 못할시 브라우저가 자동으로 생성합니다. Simple Request와 달리 OPTIONS 메서드를 통해 다른 Origin의 리소스로 HTTP 요청을 미리 보내(preflight) 실제 요청이 전송하기에 안전한지 확인합니다. 브라우저는 안전하다고 판단되면 이를 통해 실제 요청을 보내게 됩니다. Cross-Origin 요청의 경우 유저 데이터에 영향을 줄 수 있기 때문에 이와 같이 미리 전송(preflight)합니다.

이제 Preflight Request의 동작방식을 예제를 통해 알아보도록 합시다.

다음과 같은 Preflight Request를 서버로 보냈다고 가정해봅시다.
실제 요청은 POST를 사용할 것이며 요청 헤더에 Content-Type 헤더를 실어 보낼 것이다. 라는 정보를 Preflight Request 헤더에 추가해 주었습니다.

OPTIONS /about HTTP/1.1

Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
Origin: https://www.manja.com
.
.
.


브라우저가 서버의 응답을 허용할려면 서버는 다음과 같은 정보를 헤더에 실어 응답을 보내야 할 것입니다.
실제 요청에 Content-Type 헤더를 실어 보내는 것을 허용하고, POST 메소드 사용을 허용하는 정보를 헤더에 추가해 주었습니다.
브라우저는 이 응답을 보고 안전하다고 판단했기 때문에 실제 요청을 보낼 준비를 합니다.

HTTP/1.1 200 OK

Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Methods: OPTIONS, POST
.
.
.


Preflight Request를 보낸 후 안전하다고 판단하였고 브라우저는 다음과 같은 실제 요청을 보내게 됩니다.

POST /about HTTP/1.1

Content-Type: application/json
Origin: https://www.manja.com
.
.
.


서버는 응답 헤더에 Access-Control-Allow-Origin 헤더를 실어 보내고 브라우저는 Access-Control-Allow-Origin 헤더를 읽고 응답의 허용 여부를 결정하게됩니다.
Access-Control-Allow-Origin 헤더가 * 로 설정되어있기 때문에 브라우저는 해당 응답을 허용할 것입니다.

HTTP/1.1 200 OK

Access-Control-Allow-Origin: *
.
.
.


Simple Request와 Preflight Request에 대해 알아보았는데요, 위에서 HTTP 헤더 설명을 보시면 credentials라는 옵션이 나옵니다. 이 credentials 옵션은 Request with credentials 혹은 Credentialed Request 라고 불리는 인증정보를 포함한 요청을 보낼때 필요한 옵션입니다.

마지막으로 CORS의 인증정보를 포함한 요청 방식인 Credentialed Request에 대해 알아보도록 하겠습니다.

 

Credentialed Request

https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

일반적으로 같은 Origin에서 HTTP 통신을 하는 경우 알아서 쿠키가 요청 헤더에 자동으로 들어갑니다.
반대로 Origin이 다른 HTTP 통신의 경우 요청 헤더에 쿠키가 자동으로 들어가지 않습니다.
때문에 Origin이 다른 서버에 쿠키나 인증과 관련된 헤더를 담아 보낼려면 별도의 설정을 해주어야 합니다.

Credentialed Request를 하기 위해서는 두 가지 설정을 해주어야 합니다.

첫번째는 프론트 단에서 credentials 모드로 설정해 주어야 합니다.

// Fetch API를 사용할 경우
fetch(url, {
	credentials: 'include'
})

// XMLHttpRequest 객체를 사용할 경우
const xhr = new XMLHttpRequest()
xhr.withCredentials = true;

credentials 모드를 설정할 때 credentials 옵션에는 세 가지 옵션이 있습니다.

  • same-origin: 같은 Origin간에 요청에만 인증 정보를 담을 수 있다. (기본값)
  • include: 모든 요청에 인증 정보를 담을 수 있다.
  • omit: 모든 요청에 인증 정보를 담지 않는다


두번째는 서버의 응답 헤더에 다음과 같은 헤더를 추가해야 합니다.

Access-Control-Allow-Credentials: true

응답 헤더에 해당 헤더를 추가하면 credentials 정보를 담은 요청을 처리할 수 있습니다.

여기서 주의해야 할점은 Credentialed Reqeust의 경우 Access-Control-Allow-Origin 헤더를 * 로 지정해주면 안되고 명시적인 URL 로 지정해줘야 합니다.

Access-Control-Allow-Origin: https://www.manja.com


Simple Request의 경우 Preflight 과정이 없기 때문에 credentials 정보를 담은 요청을 보낼 경우 서버의 응답 헤더에 Access-Control-Allow-Credentials 헤더가 false 거나 해당 헤더가 없다면 브라우저는 서버의 응답을 거부하게됩니다.

Preflight Request의 경우 credentials 정보를 담은 요청을 보낼 경우 Preflight 과정에서 서버의 응답 헤더에 Access-Control-Allow-Credentials 헤더가 false 거나 해당 헤더가 없다면 Preflight 요청에 대한 응답은 받아오되 실제 요청에 대한 응답은 거부하게 됩니다.

참고

Comments