CORS에 대한 깊은 이해

Cross-Origin Resource Sharing의 의미, 목적 그리고 정확한 작동 원리

Lifthus
12 min readApr 5, 2023

Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources.

브라우저는 유저의 보안을 위해 기본적으로 Same-Origin Policy를 적용합니다. 이는 리소스의 프로토콜, 호스트, 포트번호가 같은 경우에만 통신을 허락하는 정책입니다. 하지만 같은 동네에서만 소통하고 살 수는 없죠. 그래서 다른 곳에 있는 리소스에 안전하게 접근할 수 있도록 HTTP 헤더에 기반한 CORS라는 장치가 주어집니다. 그리고 브라우저는 이 CORS 정책에 기반해 서로 다른 곳에서 기인한 리소스들 사이의 HTTP 통신을 필터링 합니다. 이 SOP와 CORS는 웹 개발을 처음 시작하는 분들에게 꽤나 골칫거리로 다가옵니다. 이제 이 CORS 정책이 여러 상황에서 어떻게 동작하는지 자세히 알아보도록 합시다. 시작하기 전에, 아래 내용을 먼저 읽어보시는 것을 추천드립니다.

a picture of cors + u and e
COuRSe

우선 아셔야할 것은, CORS 정책은 브라우저에 의해 실행된다는 것입니다. 서버 간의 통신에는 적용되지 않습니다. 물론 특정 Origin을 차단하는 행위는 서버에서도 충분히 가능하지만 CORS 정책 자체는 브라우저에서 브라우저 사용자를 보호하기 위한 장치입니다. 어떤 분들은 의아해하실 수 있습니다. 막상 개발을 하다보면 프론트엔드에서는 CORS에 관한 설정을 전혀 하지 않고 오히려 서버에서 CORS 관련 설정을 하기 때문입니다. 이는 입법부와 행정부의 관계에 비유할 수 있습니다. CORS 정책은 서버가 결정하고, 그 실행은 브라우저가 맡는 것이죠.

그렇다면 어떤 공격으로부터 사용자를 지키기 위해 이런 형태의 보안 장치가 탄생했을까요? 사실 CORS는 사용자를 지키는 보안 장치라기 보다는 개발자가 개발하기 편하도록 보안을 완화시켜주는 장치입니다. 그 수많은 이해할 수 없는 오류를 발생시키는 진짜 범인은 Same-Origin Policy이고 정작 CORS는 우리가 SOP를 우회해 다른 동네와 소통할 수 있도록 해주는 장치인 것이죠.

CORS는 사실 개발을 편하게 해주는 장치고 SOP가 오류의 진짜 범인 😮

어쨌건 간에, 이 CORS가 제대로 설정돼있지 않다면, 혹은 SOP가 없다면, 그냥 웹은 정상적으로 작동 불가능합니다. 어느 사이트든 일단 사용자가 접근하면 마음대로 다른 사이트에 요청을 보내도록 만들 수 있기 때문입니다. 브라우저 입장에서는 얘한테 이 요청을 보내도 되는지 알 수 없습니다. 그래서 서버에서 “아 이 사이트는 나한테 요청을 보내도 돼" 하고 브라우저에게 알려주는 것입니다.

브라우저에서는 특정 사이트에 요청을 보내도 되는지 알 방법이 없기 때문에 요청을 받는 당사자인 서버가 CORS 정책을 설정한다

그럼 이제 이 CORS를 올바르게 설정하기 위해서 클라이언트단과 서버단에서 각각 무엇을 해줘야하는지 알아보도록 합시다.

프론트엔드

아까 프론트엔드에서는 아무 설정도 하지 않는다고 했죠? 프론트엔드에서 CORS 정책에 직접적으로 관여하지는 않지만 할 일이 아예 없는건 아닙니다. 바로 요청에 credentials를 포함하는 것입니다.

브라우저는 클라이언트가 요청을 할 수 있도록 Request 인터페이스를 제공하고, 여기에는 credentials라는 read-only 속성이 있습니다. 이 속성은 해당 요청에서 credentials를 주고 받아도 되는지에 관해 지정합니다. 여기서 credentials는 쿠키, Authorization 헤더, TLS client certificates를 의미합니다. credentials 속성은 “omit”, “same-site”, “include” 세 가지 값을 허용하고 각 의미는 다음과 같습니다.

omit은 credentials를 요청에 포함하지 않습니다.

same-site는 속성의 기본값이며, 같은 사이트에 대해서만 credentials를 포함합니다.

include는 다른 사이트라도 credentials를 포함합니다.

말이 복잡해졌는데, 가장 많이 쓰이는 fetch와 axios에서 다음과 같이 간단히 credentials를 포함할 수 있습니다.

fetch('https://example.com/hello', {
method: 'GET',
credentials: 'include'
})

axios.get('https://example.com/hello', {
withCredentials: true
})

이렇게 하면, 혹은이렇게 해야 해당 요청에서 상술한 세 가지 credentials가 포함됩니다.

백엔드

백엔드에서는 프론트엔드에서 보다는 훨씬 할게 많습니다. 응답 헤더에 어떤 Origin, 어떤 Method, 어떤 Header를 허용할지 명시해줘야하며, Credentials도 허용해줘야 합니다. 백엔드가 할 일에 관해 본격적으로 알아보기 전에 Preflight 요청에 관해 알아봅시다.

사실 브라우저는 내부적으로 진짜 요청을 보내기전에 OPTIONS라는 메소드로 Preflight 요청을 보내서 CORS와 관련해 서버에게 이 요청을 보내도 되는지 미리 물어봅니다. Simple request 라고 정의되는 몇몇 경우에 대해서는 Preflight을 보내지 않지만 일단 기본적으로 Preflight Request라는게 있다는 걸 알아두시고, 많은 웹 프레임워크들은 OPTIONS 메소드를 CORS 미들웨어 등에서 처리해주지만 그렇지 않은 경우 OPTIONS 메소드에 대해서 직접 핸들링해줘야 합니다.

다음은 Go의 net/http 패키지에서 CORS와 관련해서 가장 중요한 네 가지 리스폰스 헤더들을 설정하는 예시입니다. 참고로 헤더의 속성명들은 대소문자를 구분하지 않습니다.

w.Header().Set("Access-Control-Allow-Origin", "https://example.com")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Origin")
w.Header().Set("Access-Control-Allow-Credentials", "true")

이외에도 Preflight 요청 결과를 브라우저가 캐싱하는 시간을 지정하는 Access-Control-Max-Age 같은 헤더가 있지만 필수는 아닙니다. 네 가지 헤더에 대해 자세히 알아봅시다.

Access-Control-Allow-Origin은 허용되는 Origin을 특정합니다. 오로지 단 하나의 Origin만 작성될 수 있고, 여러 오리진을 지원하는 경우 각 오리진에서 요청이 올 때마다 하나하나 헤더를 특정해줘야 합니다. 서브도메인도 이 헤더값에서는 따로 특정되야 합니다. 브라우저는 Preflight 요청의 응답 헤더 값이 유저가 요청을 보낸 Origin과 다르면 실제 요청을 보내지 않고 CORS 오류를 일으킵니다. 해당 설정을 null로 설정하는 것은 금지 의미로 사용할 수 없으니 주의하시길 바랍니다. 요청 Origin이 null이 되도록 정의된 여러 경우가 별도로 존재하고, 이에 관해서는 아래에 설명하겠습니다. 이상한 사이트에서 요청을 보내온 경우 해당 헤더를 그냥 설정하지 않으면 됩니다. 대부분의 웹 프레임워크에서 허용되는 오리진을 문자열 리스트 형태로 관리할 수 있습니다.

⚠️ Access-Control-Allow-Origin 을 null로 설정하는 것은 금지의 의미로 사용 불가

Access-Control-Allow-Methods는 허용되는 Method를 특정합니다. 콤마(,)로 구분해 여러 메소드를 허용할 수 있습니다. 마찬가지로 필터링에 사용됩니다.

Access-Control-Allow-Headers는 Preflight 요청 후 실제 요청에서 포함될 헤더를 특정합니다. 마찬가지로 콤마로 구분해 여러 헤더를 허용할 수 있으며, 실제 요청을 처리하는데 필요한 헤더들을 작성합니다.

c.f.

Access-Control-Allow-Credentials는 프론트엔드에서 설정한 Credentials가 요청에 포함돼있을 때, 이에 대한 응답을 클라이언트 코드에서 접근 가능하도록 노출할지 여부를 정합니다. 브라우저에서 Credentials를 포함해 보낸 경우 true로 설정하면 됩니다. 그리고 위 세 가지 헤더에는 모두 와일드카드(*)를 사용해 모든 값을 허용할 수 있는데, 하나라도 와일드카드를 사용할 경우 Access-Control-Allow-Credentials를 true로 설정할 수 없습니다.

⚠️ Origin, Methods, Headers 중 하나라도 와일드카드 지정시 Credentials 허용 불가

그리고 브라우저는 경우에 따라 Origin 헤더를 설정하기도 안하기도 하고, 때로는 “null”로 설정하기도 합니다. 아래에 각 경우들에 대해 나열했으니, 볼드체로 작성된 부분은 중요하거나 추후에 오류를 유발할 가능성이 큰 부분이니 꼭 기억해두시길 바랍니다.

✔ 브라우저가 Origin 헤더를 설정하는 경우

  • 다른 Origin에 대해 요청을 보낼 때
  • 같은 Origin이라도 GET, HEAD를 제외한 나머지 메소드들을 사용해 요청할 때 (즉, POST, PUT, PATCH, DELETE, OPTIONS )

❌ 브라우저가 Origin 헤더를 설정하지 않는 경우

  • 다른 Origin이지만 GET, HEAD 메소드를 통한 no-cors 모드의 요청일 때

no-cors란?

브라우저가 제공하는 Request 인터페이스에는 상술한 credentials와 같이 mode라는 read-only 속성도 존재합니다. Request의 모드는 몇 가지가 있고 이는 요청이 어떤식으로 만들어졌냐 따라 달라집니다.

간단히 설명하자면 HTML 마크업 리소스들에 의해 생성된 요청은 no-cors 모드로 설정되고 GET, HEAD 메소드를 사용하기 때문에 Origin 헤더를 포함하지 않는 반면, 클라이언트 코드에 의해 의도적으로 생성된 요청은 cors 모드로 설정되어 GET, HEAD라도 다른 Origin에 대한 요청이면 요청의 Origin을 포함합니다.

HTML 마크업 리소스는 <link>, <script>, <img> 같은 것들을 말하고 이 요소들이 만드는 요청을 no-cors 모드로 설정하고 Origin을 설정하지 않기 때문에 링크를 타고 다른 사이트로 가는 것은 CORS 오류를 발생시키지 않는 것입니다. (더 엄밀히는 그런 요청을 브라우저가 CORS로 처리하지 않기 때문에)

␀브라우저가 Origin 헤더를 null로 설정하는 경우들

  • Origin의 프로토콜이 http, https, ftp, ws, wss, gopher 중 하나가 아닐 때
  • 다른 오리진에서 기인한 미디어 데이터 ( <img>, <video>, <audio> )
  • createDocument()로, 혹은 data: URL에서 생성되거나 생성자 브라우징 컨텍스트가 없을 때
  • ⚠️ 다른 오리진에서 리다이렉트될 때
  • allow-same-origin 샌드박스 속성을 포함하지 않는 iframe
  • 네트워크 에러 응답
  • Referrer-Policy가 no-referrer로 설정돼있을 때

그럼 몇 가지 상황을 예시로 들어봅시다. 결과를 예측해 보세요.

예시

  1. abc.com에서 특정 버튼을 눌렀을 때 def.com으로 회원 탈퇴 요청을 보내고, preflight에서 Allow-Origin 응답 헤더 값으로 def.com을 받았을 때 발생하는 일
  2. 마찬가지의 상황에서 Allow-Orign 헤더 값이 hello.abc.com인 경우 발생하는 일
  3. abc.com에서 링크를 타고 def.com으로 갔을 때 일어나는 일
  4. 주소창에 def.com을 치고 들어갔을 때 발생하는 일
  5. abc.com 사이트에서 api.abc.com으로 요청을 보내고 응답으로 def.com으로 리다이렉션을 받았을 때

먼저 1번의 경우는 origin이 다르므로 당연히 CORS 에러가 발생합니다.

2번의 경우는 어떨까요? 서브도메인도 호스트가 다르므로 다른 오리진으로 간주되기 때문에 CORS에러가 발생합니다.

3번의 경우는 HTML 마크업에 의해 생성된 요소를 통한 no-cors 모드 GET 요청이기 때문에 정상적으로 접속 가능합니다.

4번의 경우와 같이 주소창에 직접 주소를 치고 들어가는 경우 Origin이 설정되지 않기 때문에 제한이 없어 정상적으로 접속 가능합니다. 브라우저에서 설정하는 요청 헤더의 Origin과 서버에서 설정하는 응답 헤더의 Access-Control-Allow-Origin을 혼동하지 마시길 바랍니다.

5번의 경우는 어떻게 될까요? api.abc.com은 abc.com과는 다른 Origin이고, 다른 Origin의 리다이렉션에 의한 요청의 Origin 헤더는 규칙에 따라서 null로 설정됩니다.

Origin header set to null with redirection by different origin
Origin header set to null with redirection by different origin

⚠️ 다른 Origin에 대한 Redirection은 Origin 헤더를 null로 만든다

해당 글에는 CORS에 관한 많은 것들을 담았습니다. 웹 개발을 이제 막 시작하려는 분들께서 이 내용을 모두 이해하시고 CORS를 자유자재로 활용하실 수 있기를 바랍니다. 😮

Conclusion

CORS는 사실 에러를 일으키는 범인이 아니라 오히려 강력한 SOP의 통제를 피해 우회할 수 있도록 길을 만들어주는 다크히어로고, 서버에 의해 CORS 정책이 정해지고 브라우저에 의해 실행된다. 또한 브라우저는 그저 HTML 화면만 띄워주는 친구가 아니라 사용자를 보호하기 위해 아주 복잡한 메커니즘을 가지고 실행하는 대단한 친구이고 CORS를 다양한 상황에서 안정적으로 활용하기 위해서는 깊은 이해가 필요하다.

--

--