본문 바로가기
정리

CORS(Cross-Origin Resource Sharing) 이해와 적용

by baau 2023. 6. 8.

이전에 프로젝트를 하면서 CORS 문제를 해결하기 위해 애먹은 적이 있다.

그 때는 CORS 문제를 해결하는 데에 집중을 해서 CORS의 이해는 조금 부족했던 것 같다. 이번에 CORS에 대해 공부할 기회가 생겨, 이번 포스팅을 통해서 CORS의 이론적인 내용 위주로 정리하고 마지막에는 한번 더 Spring에서 CORS 정책을 해결하는 방법을 기록하려고 한다.

 

SOP

  • Same-Origin Policy 의 약자
  • 다른 출처의 리소스를 사용하는 것에 제한하는 보안 방식으로, 같은 출처에서만 리소스를 공유할 수 있도록 하는 정책
  • Origin(출처)?
    • Protocol + Host + Port
    • 브라우저는 String Value로 출처를 비교하기 때문에, 127.0.01과 localhost는 다르게 판단한다.
  • 현대 웹에서는 웹 애플리케이션 간의 상호작용이 빈번하게 이루어져, 다른 출처에 있는 리소스를 가져와 사용하는 일이 흔하다.
    • 따라서 SOP을 위반하더라도 CORS를 지킨다면 다른 출처의 리소스를 사용할 수 있도록 허용한다.

 

SOP 이 필요한 이유

  • 해커의 CSRF(Cross-Site Request Forgery)나 XSS(Cross-Site Scripting) 등의 기법을 이용하여, 개인 정보를 가로챌 수 있다.
  • 예시
    • 사용자가 악성 사이트에 접속한다.
    • 해커는 악의적인 자바스크립트를 실행시켜, 사용자가 포털 사이트(Facebook, Instagram 등)로 요청을 보내도록 한다.
    • 해당 포털 사이트에서 해당 브라우저의 쿠키를 이용하여 로그인하거나 응답값을 바탕으로 사용자의 개인 정보를 가로채 탈취한다.
  • 이외에도 사용자가 접속중인 내부망의 IP와 Port를 가져오거나 해커가 사용자 브라우저를 프록시로 악용할 수 있다.

 

SOP 에 대한 오해

  • 출처 비교는 서버가 아닌 브라우저가 하는 것이다.
  • 따라서, 서버는 리소스 요청에 대해 성공적으로 응답했지만, 브라우저는 출처를 비교하여 동일 출처가 아닐 경우 응답을 보내지 않고 에러를 보낸다.
  • 이때 서버는 정상 응답으로 동작한다.

 

CORS

  • Cross-Origin Resource Sharing 의 약자 (교처 출처 리소스 공유 → 다른 출처 리소스 공유)
  • 추가 HTTP 헤더를 사용하여, 한 출처에서 실행중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제
  • 즉, SOP을 위반하더라도 CORS를 지키면 다른 출처의 리소스 요청이 가능
  • 기본 동작
    • 클라이언트는 HTTP 요청 헤더에 Origin을 담아서 전달 ex) Origin : http://localhost:3000
    • 서버는 HTTP 응답 헤더에 Access-Control-Allow-Origin을 담아 클라이언트로 전달
      ex) Access-Control-Allow-Origin : http://localhost:3000
    • 브라우저는 Origin과 Access-Control-Allow-Origin를 비교하여 응답을 내릴지, 에러를 내릴지 판단
  • 따라서 서버에서 HTTP 응답 헤더에 Access-Control-Allow-Origin를 담아 허용할 출처를 기재하면, 다른 출처라도 CORS를 지켜 다른 출처의 자원을 요청할 수 있다.

 

CORS 작동 방식 3가지 시나리오

Preflight Request (예비 요청)

  • 브라우저는 요청을 보낼 때, 본 요청전에 예비 요청을 보내 안전한 통신이 일어나는지 확인한다.

  • Preflight Request
    • Origin : 출처
    • Access-Control-Request-Method : 실제 요청의 메서드
    • Access-Control-Request-Headers : 실제 요청의 헤더들
  • Preflight Response (200, 응답 바디는 비어있음)
    • Access-Control-Allow-Origin : 허용하는 출처
    • Access-Control-Allow-Methods : 허용하는 메서드
    • Access-Control-Allow-Headers : 허용하는 헤더들
    • Access-Control-Allow-Max-Age : Preflight 응답 캐시 기간 (매번 두번의 요청이 필요하기 때문에 캐시를 확인하여 사전 확인 작업을 하지 않도록)

 

Simple Request (단순 요청)

  • 예비 요청을 생략하고 바로 본 요청을 보낸 후, 서버의 응답 헤더의 Access-Control-Allow-Origin 에 따라 CORS 위반 여부를 검사하는 방식

  • 간단한 만큼 특정 조건을 만족할 때만 예비 요청을 생략하고 본 요청을 보낼 수 있다.
    • 요청 메서드 : GET, HEAD, POST 중 하나이어야 한다.
    • 요청 헤더 : Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width 일 경우에만 적용한다.
    • Content-Type 헤더 : application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나여야한다.
  • 대부분 HTTP API 요청은 Content-Type 헤더가 text/xml 이나 application/json 이거나, 인증/인가로 인해 요청 헤더에 주로 Authorization를 사용하기 때문에 API 통신은 예비 요청으로 이루어진다.

 

Credentialed Request (인증된 요청)

  • 클라이언트에서 서버에게 자격 인증 정보(Credential)와 함께 요청할때 사용되는 요청
    • Credential : 세션 ID가 저장하고 있는 쿠키값이나 토큰
  • 브라우저가 제공하는 요청 API 들은 별도의 옵션 없이 브라우저의 쿠키와 같은 인증과 관련된 데이터를 함부로 요청 데이터에 담지 않도록 되어있다.
    • 따라서 브라우저가 제공하는 요청 API에 인증과 관련된 정보를 담을 수 있도록 옵션을 설정해야 한다.
    • Credential 옵션
      • same-origin : 같은 출처 간 요청에만 인증 정보를 담을 수 있다. (Default)
      • include : 모든 요청에 인증 정보를 담을 수 있다.
      • omit : 모든 요청에 인증 정보를 담지 않는다.
  • 서버는 Credential Request는 Prefilght Request와 Simple Request와 다르게 아래와 같이 조건이 있다.
    • Access-Control-Allow-Credentials는 true로 설정해야한다.
    • Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers는 와일드카드(*)를 사용할 수 없다.

 

CORS 적용 방법

  • 프록시 서버 사용
    • 클라이언트는 서버로 직접 요청을 하지 않고, 모든 출처를 허용하는 프록시 서버를 통해 요청한다.
    • 무료 프록시 서버는 악용 사례가 많고, api 요청이 제한되어 있어 실제로 사용하기에는 무리가 있다.
    • 직접 프록시 서버를 구축해서 사용
  • 서버에서 Access-Control-Allow-Origin 헤더 세팅 (Spring Example)
    • 스프링 서버 전역으로 CORS 설정
    • 특정 컨트롤러에만 CORS 적용 (@CrossOrigin)
// 스프링 서버 전역으로 CORS 설정
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
        	.allowedOrigins("http://localhost:8080", "http://localhost:8081")
            .allowedMethods("GET", "POST")
            .allowCredentials(true)
            .maxAge(3000)
    }
}

// 특정 컨트롤러에만 CORS 적용
@Controller
@CrossOrigin(origins = "*", methods = RequestMethod.GET) 
public class customController {

    @GetMapping("/url")  
    @CrossOrigin(origins = "*", methods = RequestMethod.GET) 
    @ResponseBody
    public List<Object> findAll(){
        return service.getAll();
    }
}
  • Apache, Tomcat, Ngix, AWS (S3 호스팅) 세팅

 

CORS 보안 가이드

  • 와일드카드(*) 출처 사용 금지한다.
  • 정규식으로 사용할 경우 주의해야 한다.
  • Origin 요청 헤더의 값을 그대로 사용 금지한다. 
    • ex) response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
  • NULL 출처 허용 금지한다.
    • iframe을 통한 공격으로 쉽게 뚫린다.
  • 화이트 리스트 사용한다.
    • 요청을 전송한 출처가 화이트 리스트에 있는 도메인 목록에 있는 경우에만 Access-Control-Allow-Origin 헤더에 해당 출처를 지정한다.
  • 클라이언트에서 미리 origin 헤더값을 위조하면 서버의 CORS를 뚫을수 있지 않을까 싶지만, 브라우저에서 이를 감지하여 차단한다.