본문 바로가기

Computer Science/네트워크

SOP, CORS - CORS 이슈 해결하기

이번 포스팅에서는 웹 애플리케이션의 보안의 핵심 개념인 SOP(Same Origin Policy)와 CORS(Cross-Origin Resource Sharing)에 대해서 살펴보고, . 이 글에서는 SOP와 CORS에 대해 알아보고, 이들이 웹 보안에 어떻게 기여하는지 살펴보겠습니다.

 

Same Origin Policy (SOP)란?

SOP는 웹 보안을 위한 가장 기본적인 정책 중 하나입니다. SOP는 브라우저에서 실행되는 스크립트 언어를 통해 동작하며, 웹 페이지의 자원에 접근하는 규칙을 정의합니다. SOP는 동일 출처(Origin)에서 로드된 문서나 스크립트만이 서로 상호작용할 수 있도록 제한합니다. Origin이란 아래 그림처럼, 프로토콜, 호스트, 포트로 구성되며, Origin이 동일하지 않으면 SOP에 의해 제한됩니다.

이해를 돕기 위해서 Same Origin인 예시와 아닌 예시를 들어보겠습니다.

Same Origin :
사용자가 브라우저에서 https://www.example.com에 접속한다고 가정해봅시다. 해당 페이지에서 JavaScript 코드가 실행됩니다. 이 페이지에서 로드된 스크립트가 다른 리소스에 접근하려고 할 때, SOP는 동일한 출처를 가진 리소스에만 접근을 허용합니다. 따라서, https://www.example.com 도메인에서 로드된 스크립트는 https://www.example.com/images/logo.png와 같은 리소스에 접근할 수 있습니다. 이는 SOP가 동일한 출처에서의 상호작용을 허용한다는 예입니다.

Cross Origin :
동일한 출처를 가지지 않는 리소스에 접근하는 경우를 살펴보겠습니다. 사용자가 브라우저에서 https://www.example.com에 접속하고, 해당 페이지에서 로드된 스크립트가 https://api.example.net/data와 같은 다른 도메인의 API에 접근하려고 한다고 가정해봅시다. 이 경우, SOP에 따라 브라우저는 이 요청을 차단합니다. 왜냐하면 출처가 다르기 때문에 SOP에 의해 허용되지 않는 상호작용이기 때문입니다. 

 

구체적으로 언제 SOP가 적용될까?

먼저 https://www.example.com에 접속하여 브라우저에 자바스크립트가 로드된 상황을 생각해보겠습니다. 

 

// 현재 스크립트의 Origin은 https://www.example.com인 상황

var req = new XMLHttpRequest();
// 리소스의 Origin인 https://api.example.com/id와 현재 Origin을 비교하여 SOP를 확인한다.
req.open("GET", "https://api.example.com/id");

// 리소스의 Origin인 https://api.example.com/id와 현재 Origin을 비교하여 SOP를 확인한다.
// SimpleRequest로 요청을 보내는 경우에 정상적으로 API 서버는 응답을 반환하고, 내용이 iframe 내부에 로드된다.
<iframe src="https://api.example.com/id"/>

위 코드와 같이 iframe에 특정 api를 로드하거나 XMLHttpRequest로 요청을 전송하는 경우를 생각할 수 있습니다. SOP가 없다면 정상적으로 데이터가 iframe 내부에 로드되고, 외부 document에서 이 iframe의 데이터에 접근하는 것도 허용됩니다. 이러한 경우 다른 사이트에 쿠키를 이용해서 요청을 전송하고 응답을 받아서 데이터를 탈취하는 악의적인 행위가 가능해집니다.

반대로, SOP가 적용되는 경우를 살펴보겠습니다. SOP가 적용되고 Origin이 일치하지 않더라도 API 서버에서 정상적인 응답을 받을 수 있습니다. 따라서 iframe 내부에 정상적으로 임베딩이 되지만, 스크립트에서 iframe 내부의 데이터를 참조하면 SOP 정책에 의해서 오류가 발생하게 됩니다. 즉, iframe을 로드할 수 있지만 내/외부를 격리하여 Cross-origin인 경우에 상호간 document 접근을 제한하는 기능이라고 할 수 있겠습니다.

참고로 iframe이 아닌 XMLhttpRequest로 요청을 보내는 경우에 대해서는 iframe에서 로드까지는 가능했던 반면에, SOP가 일치하지 않으면 read조차 불가능합니다. 그러나 일반적으로 Origin이 다르더라도 다른 Origin으로 write는 가능합니다. 이 때문에 CSRF 공격이 가능해진다고 하는데요, CSRF에 대해서도 곧 다루어 보겠습니다.

 

정리하면 아래와 같습니다.

  • Cross-origin writes는 일반적으로 가능하다. (Preflight 제외)
  • Cross-origin embedding은 일반적으로 가능하지만, 외부에서 참조 시 오류가 발생한다.
    • <script src="www.evil.com"></script>
    • <link rel="stylesheet" href="www.evil.com">
    • <img src= "...">
    • <iframe> 등
  • Cross-origin read는 일반적으로 불가능하다.

Cross-Origin Resource Sharing (CORS):

SOP를 통해서 안전한 데이터 통신이 가능해졌는데, 문제점은 API 서버에서 데이터를 요청하는 경우나 외부의 리소스가 필요한 경우에도 SOP의 적용을 받아 데이터를 read할 수 없게 되는 것입니다. 이를 해결하기 위해서 CORS 개념이 등장했는데요, CORS는 Same Origin Policy (SOP)의 제한을 우회하여 다른 출처의 리소스에 접근할 수 있는 메커니즘입니다. 웹 애플리케이션에서 다른 도메인의 리소스(예: 폰트, 이미지, API)를 요청할 때, 브라우저는 보안상의 이유로 이를 차단합니다. 그렇지만 CORS는 서버가 브라우저에게 특정 출처에서 오는 요청을 허용할 것임을 알려주는 HTTP 헤더를 추가하여 브라우저에서 이를 차단하지 못하도록 알려줍니다.

CORS는 다음과 같은 두 가지 유형의 요청으로 구분됩니다:


Simple Request (간단한 요청)

Simple Request는 아래 조건을 모두 충족할 때 발생하며, Simple Request는 요청 송/수신 한 번에 데이터의 송/수신이 완료됩니다. 이후에 설명하겠지만, Preflight 요청은 본 요청을 보내기 전에 CORS를 지원하는지 확인하기 위한 요청을 보냅니다. Simple Requst의 특징은 아래와 같습니다.

  • GET, POST, HEAD 중 하나의 메서드를 사용합니다.
  • 사용자 정의 헤더를 제외한 표준 요청 헤더만 사용합니다.
  • Content-Type은 다음 중 하나로 설정됩니다: application/x-www-form-urlencoded, multipart/form-data, text/plain.


Preflight Request (사전 요청)

Preflight Request는 본 요청을 보내기 전에 CORS를 지원하는지 확인하기 위한 요청입니다. OPTIONS 메서드로 전송되며, Preflight 요청에서 SOP를 만족하지 못한다면, 본 요청이 전송되지 않기 때문에 서버에서 응답을 처리하는 일이 발생하지 않습니다. 반면, 위의 Simple Request에서는 서버에서 Response를 작성하는 것까지 정상적으로 이루어지기 때문에 실제로 본 요청에 대한 응답이 반환되고, 그 이후에 CORS가 발생합니다. 따라서, Preflight Request를 통해 서버는 브라우저로부터 요청을 미리 확인할 수 있게 되는 것입니다.

  • GET, POST, HEAD 이외의 메서드를 사용합니다.
  • 사용자 정의 헤더를 포함합니다.
  • Content-Type이 Simple Request의 조건을 충족하지 않는 값으로 설정됩니다.

CORS 문제 해결 방법

CORS를 해결하기 위해서는 여러 가지 방법이 있습니다. 이 중에서 Spring 프레임워크를 사용하는 경우와 프록시를 통해 우회하는 방법에 대해 자세히 설명하겠습니다.


Spring CORS 설정

Spring은 CORS를 해결하기 위해 @CrossOrigin 어노테이션 사용하는 방법과, Configuration을 설정하여 전역적으로 지정하는 방법이 있습니다. @CrossOrigin 설정 방법은 Spring 컨트롤러 클래스나 메서드에 @CrossOrigin 어노테이션을 추가하여 CORS를 허용할 출처를 지정할 수 있습니다. 이 어노테이션을 사용하면 특정 출처에서 오는 요청에 대해 CORS 정책을 적용할 수 있습니다.@CrossOrigin 설정은 간편하지만 일반적인 Web application 규모에서는 컨트롤러마다 달아주어야하기 때문에 번거롭습니다. 일반적으로는, Configuration 코드를 작성하여 아래처럼 허용할 Origin과 메서드를 명시합니다. 주의해야할 점은 Credentials을 true로 지정하는 경우에 허용할 Origin을 wildcard를 사용하면 안되고 반드시 명시해주어야합니다.

@CrossOrigin(origins = "https://example.com")
@RestController
public class MyController {
    //@CrossOrigin Annotation을 이용한 방법
}


// 설정 파일을 활용한 방법
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://example.com")
                .allowedMethods("GET", "POST")
                .allowCredentials(true);
    }
}

 

 

Proxy 설정


이 외에도, 프록시를 사용하여 CORS 문제를 해결할 수도 있습니다. 이 방법은 웹 서버나 애플리케이션 서버 앞에 프록시 서버를 두고, 클라이언트의 요청을 프록시 서버로 전달하여 다른 출처 간의 통신을 가능하게 합니다. 프록시 서버는 클라이언트로부터의 요청을 받아 다른 출처로의 요청을 실행하고, 그 응답을 클라이언트에게 전달합니다. 이렇게 했을 때 브라우저 입장에서는 동일한 출처로부터 모든 리소스가 반환되기 때문에 CORS 문제가 발생하지 않게 됩니다.

 

리액트 어플리케이션 프록시 구축 흐름

  • 리액트 어플리케이션으로부터 화면을 전달받습니다. 이때 호스트는 http://localhost:{reactPort}입니다.
  • 화면 버튼을 눌렀을 때 브라우저는 리액트 어플리케이션에게 요청합니다.
  • 리액트 어플리케이션에 구축된 프록시를 통해 백엔드 서비스 API(http://localhost:{wasPort})를 호출합니다.
  • 백엔드 서비스는 요청에 대한 응답을 반환합니다.
  • 리액트 어플리케이션은 이를 다시 브라우저에게 전달합니다. 브라우저는 모든 리소스를 reactPort로부터 온 동일 오리진으로 인식합니다.



이상으로 SOP 정책, 그리고 이에 따라 자연스럽게 발생하는 CORS 문제를 해결하는 두 가지 방법을 살펴보았습니다.