본문 바로가기

Spring Boot/Security

주요 웹 보안 이슈 & Security 방어 전략 : CORS

Cross-Origin Resource Sharing

 

 - CORS의 개념, 필요성

 - Same-Origin Policy(동일 출처 정책)의 의미, 한계

 - CORS의 동작 원리(단순 요청 vs Preflight 요청) 이해

 - CORS 해결을 위한 HTTP 응답/요청 헤더의 역할

 - Security에서 제공하는 CORS 설정법


CORS(Cross-Origin Resource Sharing) 이란

 

브라우저가 CORS 정책 때문에 google.com 에서 localhost:3001/cors 로 요청보낸걸 차단했다는 에러

 

위 에러는 브라우저가 preflight 요청을 보냈는데 (Response to preflight request)

서버가 응답 헤더에 Access-Control-Allow-Origin 이걸 안넣었다 (서버가 CORS 허용 헤더를 안내려줌)

그래서 브라우저가 다른 출처의 요청을 허용하지 않아서 차단한거임 

 

  • CORS(Cross Origin Resource Sharing) 라는건 교차 출처 리소스 공유를 의미한다
  • 서로 다른 Origin을 가진 애플리케이션이 서로의 리소스에 접근할 수 있도록 해준다

Origin은 프로토콜 + 호스트 + 포트의 조합

 

Origin은 프로토콜 + 호스트 + 포트의 조합

이중에 하나라도 다르면 Cross-Origin으로 간주한다
보안상의 이유로 브라우저는 Cross-Origin HTTP 요청을 기본적으로 제한하고 있다

 

최근 웹 서버에서 정적파일과 데이터를 한번에 내려주는 구조가 아닌, 프론트엔드 서버와 API 서버를 분리해서 통신한다고 함

이러한 경우, Cross-Origin 환경이므로 CORS 오류가 발생할 수 있다


출처(Origin)와 동일 출처 정책(SOP)

1. 출처(Origin)

  • URL의 프로토콜, 호스트, 포트를 합친 값
  • 브라우저의 개발자 도구 콘솔에서 location.origin을 실행하면 확인 가능

Origin, 기본 포트는 일반적으로 생략된다

 

 

 

2. 같은 출처 vs 다른 출처

 

위의 location.origin 결과로 나온 https://b1uffer.tistory.com 을 예로 든다면

URL 결과 이유
https://b1uffer.tistory.com/about 같은 출처 Protocol, Host, Port가 동일함
https://b1uffer.tistory.com/about?q=work 같은 출처 Protocol, Host, Port 동일
http://b1uffer.tistory.com/about 다른 출처 Protocol이 다름, 기본 Port 다름
https://b1uffer.tistory.com:81/about 다른 출처 Port가 다름
https://github.com/B1uffer 다른 출처 Host가 다름 

 

 

 

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

  • 브라우저는 SOP(Same-Origin Policy)를 적용하여 다른 출처의 리소스 접근을 차단한다
  • Postman이나 다른 서버에서 API를 호출할 땐 잘 되지만, 브라우저에서만 CORS policy가 발생하는 이유가
    브라우저는 차단하고 Postman은 차단하지 않기 때문임

 

(1) SOP의 장점

외부 리소스를 무분별하게 가져오는 것을 막아, XSS-XSRF 공격을 방어할 수 있다

 

 

(2) SOP의 한계

실제 웹페이지는 외부 리소스를 자주, 많이 사용한다
이를 위해 SOP 예외로 제공되는 메커니즘이 CORS


CORS의 동작 원리

CORS는 크게 단순 요청(Simple Request), 예비 요청(Preflight Request) 두가지가 있다

 

 

1. Simple Request (단순 요청)

  • 서버에 바로 요청을 보내고, 서버는 Access-Control-Allow-Origin 헤더를 포함한 응답을 반환한다
  • 브라우저는 해당 헤더를 확인하여 요청 허용 여부를 결정한다

Simple Request, 서버에 바로 요청을 보내고 서버는 Access-Control-Allow-Origin 헤더를 포함한 응답을 200으로 보낸다

 

 

Simple Request의 조건

  1. 요청 메서드는 GET, HEAD, POST 중 하나여야함
  2. Accept, Accept-Language, Content-Language, Content-Type 등 제한된 헤더만 사용 가능함
  3. Content-Type은 application/x-www-form-urlencoded, multipart/form-data, text/plain 만 허용된다

대부분의 REST API는 application/json을 사용하기 때문에 Preflight 요청(예비 요청)으로 처리된다고 함

 

 

 

2. Preflight Request(예비 요청)

  • 본요청 전에 OPTIONS 메서드로 예비 요청을 보낸다
  • 서버가 허용 여부(Access-Control-Allow-* 헤더) 를 응답하면, 브라우저가 본 요청을 진행한다

본요청 전에 OPTIONS 메서드를 통해 예비 요청을 보낸 뒤, 서버에서 허용 여부를 응답해주면 본요청을 바로 보낸다

 

GET, POST, PUT, DELETE 등의 메서드로 API를 요청했는데
크롬 개발자 도구의 네트워크 탭에 OPTIONS 메서드로 요청이 보내지는걸 경험했다면 CORS를 경험한 것임

Preflight 요청은 실제 리소스를 요청하기 전에 OPTIONS 라는 메서드를 통해 실제 요청을 전송할지 판단한다

 

OPTIONS 메서드로 서버에 예비 요청을 보내고, 서버는 이 예비 요청에 대한 응답으로 Access-Control-Allow-Origin 헤더를 포함한 응답을 브라우저에 보낸다. 브라우저는 단순 요청과 동일하게 Access-Control-Allow-Origin 헤더를 확인해서 CORS 동작을 수행할지 판단하게 된다

 


CORS 오류 해결법

CORS 오류는 단순히 서버가 응답에 CORS 관련 헤더를 포함하지 않아서 발생하는 오류이다

따라서 서버에서 적절한 Access-Control-Allow-헤더 를 설정해줘야한다

 

1. Access-Control-Allow-Origin

  • 브라우저가 리소스를 접근할 수 있는 출처를 지정한다
  • (와일드카드) 를 쓰면 모든 출처에 대해 허용한다
Access-Control-Allow-Origin: <https://b1uffer.tistory.com>
Access-Control-Allow-Origin: * # 와일드카드

 

 

2. Access-Control-Allow-Methods

  • 허용할 HTTP 메서드를 지정한다
Access-Control-Allow-Methods: GET, POST, PUT, DELETE

 

 

3. Access-Control-Expose-Headers

  • 브라우저 JavaScript 코드에서 접근 가능한 응답 헤더를 지정한다
Access-Control-Expose-Headers: X-Custom-Header, X-Another-Header

 

 

4. Access-Control-Allow-Headers

  • 브라우저가 보낼 수 있는 요청 헤더를 지정한다
Access-Control-Allow-Headers: X-Custom-Request

 

 

5. Access-Control-Max-Age

  • Preflight 요청 결과를 캐싱(?)할 시간을 지정한다
Access-Control-Max-Age: 3600

 

 

6. Access-Control-Allow-Credentials

  • 쿠키, 인증 정보를 포함한 요청을 허용할지에 대한 여부를 설정한다
Access-Control-Allow-Credentials: true

 

이 경우 Access-Control-Allow-Origin 에 * 대신 특정 출처를 지정해야한다

 


Spring Security에서 CORS 지원

Security에서는 http.cors() 와 CorsConfigurationSource 를 통해 CORS를 지원하고 있다

 

    @Bean
    @Order(3)
    public SecurityFilterChain corsFilterChain(HttpSecurity http) throws Exception {
        http
                .cors(Customizer.withDefaults()) // CORS 활성화하기
                .authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("http://localhost:3000"));
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

 


  • 개발 단계에서는 Access-Control-Allow-Origin : * 를 해도 되지만, 운영에서는 반드시 특정 출처만 허용해야한다
  • Access-Control-Allow-Credentials: true 를 사용할 경우, 와일드카드 (*) 는 사용할 수 없다
  • CDN, API Gateway 등 중간 계층에서 CORS를 미리 처리하는 경우도 많다
  • CORS 정책은 보안 기능이므로, 무분별하게 허용하지 말고 최소 권한 원칙을 지켜야함

 


정리

항목 설명
SOP 브라우저가 다른 출처 리소스 접근을 차단하는 보안 정책
CORS SOP의 예외로, 교차 출처 리소스 공유 허용
Simple Request 조건 충족시 바로 요청 + Access-Control-Allow-Origin 확인
Preflight Request OPTIONS 요청으로 사전 확인 후 본 요청 진행
주요 응답 헤더 Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Allow-Credentials,
Access-Control-Max-Age 등
Spring Security http.cors() + CorsConfigurationSource 로 손쉽게 지원