티스토리 뷰

728x90
반응형

CORS ?

CORS란 Cross-Origin Resource Sharing의 준말로, 우리나라말로 직역하면 교차 출처 자원 공유다. 여기서 교차 출처라는 의미가 가장 헷갈릴 것이다. 교차된다는 것은 서로 다르다는 의미로 생각하면 된다. 다시 말해, 다른 출처에서 온 자원을 공유한다는 의미다. 그럼 출처는 무엇일까?

Origin(출처)

출처를 이루는 요소는 URL에서 프로토콜 + 호스트 + 포트다.

https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F

출처 : 인파님 블로그

위 그림에서 Origin을 뽑아보면, HTTPS://www.domain.com:3000 이 된다.

아래 표는 URL http://store.company.com/dir/page.html의 출처를 비교한 예시다.

URL 결과 이유
http://store.company.com/dir2/other.html 동일 출처 경로만 다름
http://store.company.com/dir/inner/another.html 동일 출처 경로만 다름
https://store.company.com/page.html 실패 다른 프로토콜
http://store.company.com:81/dir/page.html 실패 다른 포트 (http:// 는 기본적으로 80 포트)
http://news.company.com/dir/page.html 실패 다른 호스트
💡WHATWG(Web Hypertext Application Technology Working Group)에서는 Origin을 다음과 같이 정의하고 있다. 출처는 웹 보안 모델의 기본 통화로, 출처를 공유하는 웹 플랫폼의 두 행위자는 서로를 신뢰하고 동일한 권한을 가지고 있다고 가정한다.

브라우저는 보통 하나의 외부로부터 자원을 가져와서 DOM을 구성해야 하는 경우가 많다.

https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F

위 그림에서 domain-a.com은 출처가 같지만, domain-b.com의 경우 출처가 다르다. 브라우저는 이 출처를 기준으로 정책을 정하는데, CORS는 출처가 다른 서버로부터 자원을 가져와야 할 때 사용되는 정책이다.

동일 출처 정책 vs CORS 정책

동일 출처 정책

동일 출처 정책SOP(Same Origin Policy)로, 동일한 출처에서만 리소스를 공유할 수 있다. 보안 상의 이유로 브라우저는 스크립트 요청 시 출처가 다른 HTTP 요청을 제한하고 있다. 대표적으로 fetch()XMLHttpRequest 를 사용할 때, 동일 출처 정책을 따른다.

사실 출처가 다른 두 어플리케이션이 자유로이 소통할 수 있는 환경은 꽤 위험한 환경이다. 예를 들어 인터넷의 악의적인 웹사이트가 브라우저에서 JS를 실행하여 (사용자가 로그인 한) 타사 웹메일 서비스나 회사 인트라넷에서 데이터를 읽고 공격자에게 전달할 수 있다.

출처를 비교하는 로직은 서버에 구현된 스펙이 아닌브라우저에 구현된 스펙이다.

https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F

출처 : 인파님 블로그

위의 경우 서버는 정상적으로 응답을 했지만, 브라우저 입장에서는 호스트가 다른 출처로부터 응답이 왔기 때문에 차단한다.

하지만 인터넷 상에서 웹 사이트를 구성하려면 여러 출처로부터 리소스를 가져오는 것은 필수불가결한 사항이다. 따라서 무턱대고 다 막을 수는 없다. 그래서 몇 가지 예외 사항을 둔 정책이 CORS 정책이다.

CORS 정책

브라우저가 자신의 출처가 아닌 다른 출처로부터 자원을 로딩하는 것을 허용하도록 서버가 허가 해주는 HTTP 헤더 기반 메커니즘이다.

예를 들어 다음과 같은 경우 출처가 다르더라도 HTTP 요청을 허가해 준다.

  • <img>, <video>, <script>, <link> 태그
  • fetch() 또는 XMLHttpRequest의 호출.
  • 웹 폰트(CSS 내 @font-face에서 교차 도메인 폰트 사용 시)
  • WebGL 텍스쳐
  • drawImage()를 사용해 캔버스에 그린 이미지/비디오 프레임
  • 이미지로부터 추출하는 CSS Shapes

CORS 정책을 제대로 이해하려면 “서버가 허가 해주는 HTTP 헤더 기반 메커니즘” 이라는 말을 제대로 이해해야 한다. 아래는 브라우저가 CORS 정책을 사용하는 간단한 시나리오다.

  1. 클라이언트에서 HTTP 요청의 헤더에 Origin을 담아 전달
  • 기본적으로 웹은 HTTP 프로토콜을 이용하여 서버에 요청을 보내게 되는데,
  • 이때 브라우저는 요청 헤더에 Origin 이라는 필드에 출처를 함께 담아 보내게 된다.

https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F

출처 : 인파님 블로그

  1. 서버는 응답헤더에 Access-Control-Allow-Origin 을 담아 클라이언트로 전달한다.
  • 이후 서버가 이 요청에 대한 응답을 할 때 응답 헤더에 Access-Control-Allow-Origin 이라는 필드를 추가하고 값으로 '이 리소스를 접근하는 것이 허용된 출처 url'을 내려보낸다.

https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F

출처 : 인파님 블로그

  1. 클라이언트에서 Origin과 서버가 보내준 Access-Control-Allow-Origin 을 비교한다.
  • 이후 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin 을 비교해본 후 차단할지 말지를 결정한다.
  • 만약 유효하지 않다면 그 응답을 사용하지 않고 버린다. (CORS 에러)
  • 위의 경우에는 둘다 http://localhost:3000 이기 때문에 유효하니 다른 출처의 리소스를 문제없이 가져오게 된다.

위 시나리오는 개념적으로 간단히 보인 것이고, 실제로 동작하는 방식으로는 세 가지 시나리오가 있다.

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

단순 요청(Simple requests)

단순 요청 시나리오는 다음 조건을 모두 만족해야 한다.

  • 다음 중 하나의 메서드 : GET, HEAD, POST
  • 다음 헤더
    • Accept
    • Accept-Language
    • Content-Language
    • Range
  • Content-Type 헤더가 아래와 같을 때
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

예를 들어 Content-Type: application/json 인 rest api 요청같은 경우, 단순 요청 시나리오에 포함되지 않는다.

https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

  • 브라우저의 HTTP 요청
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: [https://foo.example](https://foo.example/)

요청 헤더에서 Origin 헤더 값을 보면 출처를 알 수 있다.

  • 서버 응답
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

[…XML Data…]

서버는 Access-Control-Allow-Origin 헤더 값으로 허용할 출처들을 담을 수 있는데, * 는 모든 출처를 허용하겠다는 의미다.

사전 요청(Preflighted requests)

단순 요청과 달리 사전 전송(preflighted) 요청의 경우 실제 요청을 보내는 것이 안전한지 판단하기 위해 브라우저가 먼저 OPTIONS 메서드를 사용해 다른 출처의 리소스에 HTTP 요청을 보낸다. 사실 단순 요청보다 더 자주 사용되는 시나리오다.

https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

OPTIONS 는 서버로부터 추가 정보를 얻기 위해 사용되는 HTTP/1.1 메서드이며 리소스를 변경할 수 없는 안전한 메서드다.

  • Access-Control-Request-Method 헤더는 사전 요청의 일부로써, 서버에게 실제 요청이 전송될 때 POST 요청 메서드를 사용할 것임을 알리고 있다.
  • Access-Control-Request-Headers 헤더는 실제 요청이 전송될 때 사용자 정의 헤더 X-PINGOTHERContent-Type 을 사용할 것임을 서버에게 알린다.

서버가 응답한 헤더를 보자.

  • Access-Control-Allow-Origin: https://foo.example 헤더로 응답하여 요청을 보낸 출처 도메인만 접근 가능하도록 제한한다.
  • Access-Control-Allow-Methods 헤더로 응답하여 POSTGET 메서드가 해당 리소스를 요청하는 데 유효한 메서드임을 나타낸다.
  • Access-Control-Allow-Headers 헤더에 X-PINGOTHER, Content-Type 값을 설정하여 보내, 이 헤더들이 허용된 헤더임을 확인
  • Access-Control-Max-Age 는 또 다른 사전 요청을 보내지 않도록 사전 요청에 대한 응답을 얼마나 오래동안 캐시할 수 있는지 초 단위 시간 값을 제공

자격 증명을 포함한 요청

자격 증명을 포함한 요청은 클라이언트에서 서버에게자격 인증 정보(Credential)를 실어 요청할때 사용되는 요청이다. 여기서 말하는자격 인증 정보란 세션 ID가 저장되어있는 쿠키(Cookie) 혹은 Authorization 헤더에 설정하는 토큰 값 등을 일컫는다.

즉, 클라이언트에서 일반적인 JSON 데이터 외에도 쿠키 같은 인증 정보를 포함해서 다른 출처의 서버로 전달할 때 CORS의 세가지 요청 중 하나인 인증된 요청으로 동작된다는 말이며, 이는 기존의 단순 요청이나 예비 요청과는 살짝 다른 인증 형태로 통신하게 된다.

fetch() 요청에 자격 증명을 포함하려면, credentials 옵션을 include 로 설정해야 한다.

const url = "https://bar.other/resources/credentialed-content/";

const request = new Request(url, { credentials: "include" });

const fetchPromise = fetch(request);
fetchPromise.then((response) => console.log(response));

https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

추가로 서버에서 설정하는 헤더에 몇 가지 제약이 생긴다.

  • 응답 헤더의 Access-Control-Allow-Credentials 항목을 true로 설정해야 한다.
  • 응답 헤더의 Access-Control-Allow-Origin 의 값에 와일드카드 문자("*")는 사용할 수 없다.
  • 응답 헤더의 Access-Control-Allow-Methods 의 값에 와일드카드 문자("*")는 사용할 수 없다.
  • 응답 헤더의 Access-Control-Allow-Headers 의 값에 와일드카드 문자("*")는 사용할 수 없다.

응답의 Access-Control-Allow-Origin 헤더가 와일드카드(*)가 아닌 분명한 Origin으로 설정되어야 하고, Access-Control-Allow-Credentials 헤더는 true 로 설정되어야 한다는 뜻이다. 그렇지 않으면 브라우저의 CORS 정책에 의해 응답이 거부된다. (인증 정보는 민감한 정보이기 때문에 출처를 정확하게 설정해주어야 한다)

💡위 3가지 시나리오 예제를 테스트할 수 있는 사이트 : https://chuckchoiboi.github.io/cors-tutorial/

Spring에서 CORS 설정

필자가 Spring을 사용하기 때문에, Spring에서 CORS를 설정하는 방법 몇 가지를 간단하게 소개해 본다.

Controller Method CORS Configuration

@CrossOrigin(origins = "http://localhost:9000")
@GetMapping("/greeting")
public Greeting greeting(@RequestParam(required = false, defaultValue = "World") String name) {
        // ...
}

import org.springframework.web.bind.annotation.CrossOrigin 애노테이션은 다음과 같다.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {

    @AliasFor("origins")
    String[] value() default {};

    @AliasFor("value")
    String[] origins() default {};  // 허용할 출처(Origin) 목록    

    String[] originPatterns() default {};  // 패턴으로 지정

    String[] allowedHeaders() default {};  // 클라이언트 요청 시 허용할 헤더    

    String[] exposedHeaders() default {};  // 클라이언트에 노출할 응답 헤더    

    RequestMethod[] methods() default {};  // 허용할 HTTP 메서드    

    String allowCredentials() default "";  // 인증 정보(Cookie, Authorization 헤더 등) 전송 여부    

    String allowPrivateNetwork() default "";

    long maxAge() default -1;  // Preflight 요청 결과를 캐시할 시간(초)    

}

Global CORS configuration

@Configuration 
public class WebMvcConfig implements WebMvcConfigurer {

        private final long MAX_AGE_SECS = 3600;

        @Override
        public void addCorsMappings(CorsRegistry registry) {
            // 모든 경로에 대해
            registry.addMapping("/**")
                    // Origin이 http:localhost:3000에 대해
                    .allowedOrigins("http://localhost:3000")
                    // GET, POST, OPTIONS 메서드를 허용
                    .allowedMethods("GET", "POST","OPTIONS")
                    // Content-Type 헤더를 허용
                    .allowedHeaders("Content-Type")
                    // 자격 증명 허용
                    .allowCredentials(true)
                    // 사전 요청 캐시 만료 시간 지정
                    .maxAge(MAX_AGE_SECS);
        }
}

Spring Security

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors() // CORS 활성화
            .and()
            .csrf().disable() // 필요 시 CSRF 비활성화
            .authorizeHttpRequests()
            .anyRequest().permitAll(); // 모든 요청 허용 (상황에 따라 조정)

        return http.build();
    }

    @Bean
        public UrlBasedCorsConfigurationSource corsConfigurationSource() {
                CorsConfiguration configuration = new CorsConfiguration();
                // 허용할 출처
                configuration.setAllowedOrigins(Arrays.asList("https://example.com")); 
                // 허용할 HTTP 메서드 
                configuration.setAllowedMethods(Arrays.asList("GET","POST"));
                // 쿠키 전송 허용
            	configuration.setAllowCredentials(true);

                UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
                // 모든 경로에 적용
                source.registerCorsConfiguration("/**", configuration);
                return source;
        }
}
  • CSRF 비활성화는 필요 시에만 적용
  • setAllowCredentials(true)를 사용할 경우, allowedOrigins* 사용 불가

fetch-metadata request header로 서버 보안도 강화하기

CORS 정책을 적용해 다른 출처의 리소스 응답을 거절함으로써 보안을 강화하는 것은 결국 브라우저의 몫이다. 그에 반해, 서버 입장에서는 악의적인 요청이라도 허용할 Origin 목록을 담아 결국 응답을 하게 된다. 언뜻봐도 내키지 않는 상황이다.

최신 브라우저 환경에서는 fetch-metadata 요청 헤더가 지원된다.

  • Chrome 76
  • Edge 79
  • Firefox 90
  • Safari 16.4

fetch-metadata 요청 헤더는 서버가 CSRF같은 교차 출처 공격으로부터 도움을 줄 수 있도록 설계된 새로운 웹 플랫폼 보안 기능이다. Sec-Fetch-* 헤더 집합에 HTTP 요청의 컨텍스트에 관한 정보를 제공하면 응답 서버가 요청을 처리하기 전에 보안 정책을 적용할 수 있습니다. 즉, 출처가 의심되는 요청에 대해서는 서버가 굳이 응답하지 않고 다른 처리를 할 수 있게 된 것이다. 또한, Sec-* 헤더들은 javascript로 수정도 불가하다.

위에서 same-orgin과 cross-origin에 대해서 알아봤다. 추가로 same-site와 cross-site에 대해서도 알아야 한다.

Same-site

site가 같다는 의미는 origin과는 약간 다르다.

  • https://www.example.com:433

위 URL을 구분해 보면 다음과 같다.

여기서 host를 좀 더 구분해 보자.

  • TLD : com
  • TLD + 1 : example.com

여기서 TLD(Top Level Domain)란 최상위 도메인을 의미한다. 이 최상위 도메인 중에서도 유효 최상위 도메인(eTLD)들은 https://publicsuffix.org/list/ 에서 관리되는 도메인들을 말한다.

사이트는 스키마와 eTLD, 그리고 eTLD + 1까지를 의미한다.

사이트(site) = scheme + (eTLD + 1). eTLD

위 예시의 경우, 사이트는 https://example.com 이 된다. 다른 예로 https://www.project.github.io:433 의 경우, eTLD가 .github.io 기 때문에 사이트는 https://project.github.io 가 된다.

출처 A 출처 B 결과
https://www.example.com:443 https://www.evil.com:443 크로스 사이트: 서로 다른 도메인
  https://login.example.com:443 동일 사이트: 하위 도메인이 다르더라도 상관없음
  http://www.example.com:443 크로스 사이트: scheme이 다름
  https://www.example.com:80 동일 사이트: 포트가 다르더라도 상관없음
  https://www.example.com:443 동일 사이트: 일치
  https://www.example.com 동일 사이트: 포트는 중요하지 않음

출처와 가장 큰 차이는 포트와 하위 도메인을 구분하지 않는다는 것이다.

💡원래 사이트는 스키마를 포함하지 않았다. 하지만 그러면 보안에 문제가 되므로 스펙이 바뀌면서, 스키마를 포함한 사이트(schemeful-same-site)가 나와 스키마가 다르면 cross-site로 간주했다. 이제는 사이트를 판별할 때 스키마를 포함하는게 원칙이 됐다.

fetch-metadata 요청 헤더 알아보기

Sec-Fetch-Site

서버에 요청을 보낸 사이트를 알려 준다. resource에 대한 요청이 동일한 출처, 동일한 사이트, 다른 사이트에서 오는지 아니면 사용자가 시작한 요청인지의 여부를 서버에 알려준다.

  • same-origin: 자체 애플리케이션에서 요청한 경우 (예: site.example)
  • same-site: 사이트의 하위 도메인 (예: bar.site.example)에서 요청한 경우
  • none: 사용자가 사용자 에이전트와 상호작용하여 요청이 명시적으로 발생한 경우 (예: 주소창에 URL 입력, 북마크 클릭)
  • cross-site: 다른 웹사이트에서 요청을 전송한 경우

Sec-Fetch-Mode

요청의 모드를 나타낸다. 서버가 HTML 페이지를 이동하는 사용자의 요청과 이미지 및 기타 자원의 요청을 구별할 수 있도록 한다. 예를 들어 대상이 navigate이면 최상위 탐색 요청을 나타내고, no-cors이면 이미지 로드와 같은 리소스 요청을 나타낸다.

  • cors: CORS protocol 요청
  • navigate: HTML document 사이를 이동할 때 사용
  • no-cors: no-cors 요청
  • same-origin: 요청 중인 resource와 동일한 출처
  • websocket: websocket 연결을 설정하기 위한 요청

Sec-Fetch-Dest

요청의 대상을 나타낸다. 이는 fetch 요청 개시자로, 가져온 data가 사용되는 곳이다.

위 메타 데이터 헤더들을 이용해 서버는 정상 응답을 할 지, 거부할 지 판단할 수 있게 된다. 예를 들어, 리소스 격리 정책을 사용해, 외부 웹사이트에서 리소스를 요청하지 못하도록 할 수 있다. 이러한 트래픽을 차단하면 CSRF, XSSI, 타이밍 공격, 크로스 출처 정보 유출과 같은 일반적인 교차 사이트 웹 취약점을 완화할 수 있다.


References

728x90
반응형

'CS > Network' 카테고리의 다른 글

HTTP 메서드 & 상태코드 & 헤더  (0) 2024.08.10
What is HTTP?  (0) 2024.07.21
[네트워크] ARP (Address Resolution Protocol)  (0) 2023.03.26
[네트워크] OSI 7 계층 개요  (1) 2023.03.26
[네트워크] 소켓 프로그래밍 개요  (0) 2023.03.26
댓글