위의 내용을 본 적이 있나요? 아마도… 그리고 아마도 꽤 많이…봤을거에요.
위의 오류를 수정하는 방법을 설명하는 수백만 개의 문서가 있지만 "Cross-Origin Resource Sharing(CORS)"는 정확히 무엇이며 왜 존재하는 것일까요?
왜??
먼저 시나리오를 통해 그 이유에 대한 질문에 답하고 다양한 시점에 어떻게 작동하는지 살펴봅시다.
뱅킹 서비스인 bank.com
에 로그인한다고 상상해 보세요. 로그인하면 '세션 쿠키(Session Cookie)'가 브라우저에 저장됩니다. (세션 쿠키는 기본적으로 bank.com
뒤에 있는 서버에 브라우저가 계정에 로그인했음을 알려줍니다). 앞으로 bank.com
에 대한 모든 요청에는 이 쿠키가 포함되며, bank.com
은 사용자가 로그인한 것을 알고 제대로 응답할 수 있습니다.
이제 메일함을 확인해 보겠습니다. 의심스러운 이메일이 보이면 당연히 그 안에 있는 링크를 클릭해 attack.com
으로 이동합니다. 다음으로, 이 웹사이트는 은행 정보를 얻기 위해 bank.com
에 요청을 보냅니다. 이 세션 쿠키가 브라우저에 저장되어 있기 때문에 bank.com
은 여전히 사용자가 로그인한 것으로 간주합니다. bank.com
뒤에 있는 서버는 사용자가 정상적으로 뱅킹 정보를 요청한 것처럼 보이므로 이를 다시 전송합니다. 이제 공격자는 이 쿠키에 액세스하여 악의적으로 다른 곳에 저장합니다.
사람들은 이것이 좋지 않다는 것을 깨달았고, 브라우저는 SOP(Same-Origin Policy; 동일-출처 정책)를 채택하여 사용자가 bank.com
이 아닌 다른 곳에서 bank.com
에 요청을 시도하는 것을 브라우저가 감지하면 이를 차단합니다. 여기서 중요한 점은 브라우저 기반 정책이라는 점입니다. bank.com
은 요청의 출처를 알 수 있는 방법이 없기 때문에 CSRF와 같은 공격으로부터 크게 보호할 수 없습니다. 사용 중인 브라우저는 기본적으로 Origin에 대한 세부 정보(스키마 + 도메인 이름 + 포트, https//foo.com:4000, http//bar.org:3000 등… 기본적으로 URL)를 요청하는 것처럼 보이는 경우 동일한 Origin의 요청만 전송한다고 말합니다.
이 기능은 훌륭하지만 매우 제한적이었습니다. 즉, 공개 API는 전혀 작동하지 않았습니다. 일종의 프록시 솔루션을 사용하지 않는 한 데이터를 요청할 수 없었습니다.
CSRF
서버는 요청이 어디에서 왔는지 어느 정도 알 수 있습니다. 요청에는 어떤 출처에서 요청을 했는지 보여주는 ‘Origin’ 헤더가 있어야 합니다. 예를 들어, 위의 예에서 요청은 다음과 같이 보입니다.
1 | // -----> bank.com으로 요청 |
이론적으로 bank.com
은 이를 확인하여 출처가 합당한 요청에만 응답해야 합니다. 그리고 보통은 그렇게 하므로 SOP는 다소 제한적인 것 같습니다.
바로 이때 CORS가 등장합니다.
CORS
example.com
의 웹 애플리케이션이 bank.com
에서 리소스를 요청하려고 하면 브라우저는 요청에 요청의 출처(example.com
)를 나타내는 Origin 헤더를 자동으로 포함합니다. 여기서 중요한 부분이 있습니다: bank.com
의 서버는 SOP에 따라 이러한 cross-origin 요청을 전면적으로 차단하는 대신 이 Origin 헤더를 검사하고 자체 CORS 정책에 따라 요청을 허용할지 거부할지 결정할 수 있습니다.
example.com
이 신뢰할 수 있거나 요청되는 리소스가 공개적으로 액세스할 수 있는 리소스라고 판단되는 경우, bank.com
은 리소스에 액세스할 수 있는 출처를 나타내는 Access-Control-Allow-Origin
과 같은 특정 CORS 헤더를 사용하여 응답할 수 있습니다. 이 헤더는 이 Origin을 명시적으로 허용하는 http://example.com
또는 모든 Origin이 액세스할 수 있는 공개 리소스의 경우 *
로 설정할 수 있습니다.
물론 브라우저는 이 모든 작업을 용이하게 합니다. 이 중 하나라도 잘못되면 불쾌한 오류가 발생합니다.
자 이제 요청에 Origin 헤더가 없으면 어떻게 될까요? 다른 헤더가 많고 기본 HTTP 메서드 중 하나를 사용하지 않는다면 어떨까요?
이러한 상황에서는 더 이상 "Simple Request"가 아니므로 CORS 처리가 조금 더 복잡해집니다. 바로 이 지점에서 CORS의 “Preflight” 요청이라는 개념이 등장합니다.
Preflight
서버의 데이터를 수정할 수 있는 특정 타입의 요청(PUT, DELETE와 같은 HTTP 메서드를 사용하거나 모든 요청에 자동으로 포함되지 않는 헤더를 사용하는 요청)의 경우 브라우저는 실제 요청을 하기 전에 먼저 “Preflight” 요청을 보냅니다. 이 Preflight 요청은 HTTP 옵션 요청으로, 실제 요청을 전송해도 안전한지 서버에 확인하는 데 목적이 있습니다.
Preflight 요청에는 HTTP 메소드를 설명하는 헤더와 실제 요청의 헤더가 포함됩니다. 다음과 같습니다.
- 서버 응답: 서버가 CORS 정책과 실제 요청을 지원하는 경우, 서버는 허용되는 메소드와 헤더를 나타내는 헤더를 사용하여 Preflight 요청에 응답합니다. 여기에는
Access-Control-Allow-Methods
및Access-Control-Allow-Headers
와 같은 헤더가 포함될 수 있습니다. - 브라우저 결정: Preflight에 대한 서버의 응답에 따라 브라우저는 실제 요청을 진행할지 여부를 결정합니다. 서버의 응답이 요청이 허용됨을 나타내면 브라우저는 요청을 전송하고, 허용되지 않으면 브라우저는 요청을 차단하고 CORS 관련 오류가 표시됩니다.
결론
이제 CORS에 대해 조금 더 이해하셨기를 바랍니다. 가장 중요한 점은 이것이 모두 브라우저 정책이며 서버가 이를 준수하도록 코딩되어야 한다는 것입니다. 이는 사용자의 안전을 지키기 위해 마련된 것입니다. Chrome을 사용하는 경우 잘못된 링크를 클릭하는 것에 대해 크게 걱정할 필요는 없습니다(물론 여전히 조금은 걱정해야 합니다). 하지만 완벽한 정책은 아닙니다. 표준을 준수하지 않는 타사 브라우저를 사용하면 이 모든 것이 없어질 수 있습니다. 그렇기 때문에 어떤 소프트웨어를 사용하는지 신중해야 합니다!