티스토리 뷰
CORS ?
CORS란 Cross-Origin Resource Sharing의 준말로, 우리나라말로 직역하면 교차 출처 자원 공유다. 여기서 교차 출처라는 의미가 가장 헷갈릴 것이다. 교차된다는 것은 서로 다르다는 의미로 생각하면 된다. 다시 말해, 다른 출처에서 온 자원을 공유한다는 의미다. 그럼 출처는 무엇일까?
Origin(출처)
출처를 이루는 요소는 URL에서 프로토콜 + 호스트 + 포트다.
위 그림에서 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을 구성해야 하는 경우가 많다.

위 그림에서 domain-a.com은 출처가 같지만, domain-b.com의 경우 출처가 다르다. 브라우저는 이 출처를 기준으로 정책을 정하는데, CORS는 출처가 다른 서버로부터 자원을 가져와야 할 때 사용되는 정책이다.
동일 출처 정책 vs CORS 정책
동일 출처 정책
동일 출처 정책은 SOP(Same Origin Policy)로, 동일한 출처에서만 리소스를 공유할 수 있다. 보안 상의 이유로 브라우저는 스크립트 요청 시 출처가 다른 HTTP 요청을 제한하고 있다. 대표적으로 fetch()
와 XMLHttpRequest
를 사용할 때, 동일 출처 정책을 따른다.
사실 출처가 다른 두 어플리케이션이 자유로이 소통할 수 있는 환경은 꽤 위험한 환경이다. 예를 들어 인터넷의 악의적인 웹사이트가 브라우저에서 JS를 실행하여 (사용자가 로그인 한) 타사 웹메일 서비스나 회사 인트라넷에서 데이터를 읽고 공격자에게 전달할 수 있다.
출처를 비교하는 로직은 서버에 구현된 스펙이 아닌브라우저에 구현된 스펙이다.
위의 경우 서버는 정상적으로 응답을 했지만, 브라우저 입장에서는 호스트가 다른 출처로부터 응답이 왔기 때문에 차단한다.
하지만 인터넷 상에서 웹 사이트를 구성하려면 여러 출처로부터 리소스를 가져오는 것은 필수불가결한 사항이다. 따라서 무턱대고 다 막을 수는 없다. 그래서 몇 가지 예외 사항을 둔 정책이 CORS 정책이다.
CORS 정책
브라우저가 자신의 출처가 아닌 다른 출처로부터 자원을 로딩하는 것을 허용하도록 서버가 허가 해주는 HTTP 헤더 기반 메커니즘이다.
예를 들어 다음과 같은 경우 출처가 다르더라도 HTTP 요청을 허가해 준다.
<img>
,<video>
,<script>
,<link>
태그fetch()
또는XMLHttpRequest
의 호출.- 웹 폰트(CSS 내
@font-face
에서 교차 도메인 폰트 사용 시) - WebGL 텍스쳐
drawImage()
를 사용해 캔버스에 그린 이미지/비디오 프레임- 이미지로부터 추출하는 CSS Shapes
CORS 정책을 제대로 이해하려면 “서버가 허가 해주는 HTTP 헤더 기반 메커니즘” 이라는 말을 제대로 이해해야 한다. 아래는 브라우저가 CORS 정책을 사용하는 간단한 시나리오다.
- 클라이언트에서 HTTP 요청의 헤더에 Origin을 담아 전달
- 기본적으로 웹은 HTTP 프로토콜을 이용하여 서버에 요청을 보내게 되는데,
- 이때 브라우저는 요청 헤더에
Origin
이라는 필드에 출처를 함께 담아 보내게 된다.
- 서버는 응답헤더에
Access-Control-Allow-Origin
을 담아 클라이언트로 전달한다.
- 이후 서버가 이 요청에 대한 응답을 할 때 응답 헤더에
Access-Control-Allow-Origin
이라는 필드를 추가하고 값으로 '이 리소스를 접근하는 것이 허용된 출처 url'을 내려보낸다.
- 클라이언트에서 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 요청같은 경우, 단순 요청 시나리오에 포함되지 않는다.

- 브라우저의 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 요청을 보낸다. 사실 단순 요청보다 더 자주 사용되는 시나리오다.

OPTIONS
는 서버로부터 추가 정보를 얻기 위해 사용되는 HTTP/1.1 메서드이며 리소스를 변경할 수 없는 안전한 메서드다.
Access-Control-Request-Method
헤더는 사전 요청의 일부로써, 서버에게 실제 요청이 전송될 때POST
요청 메서드를 사용할 것임을 알리고 있다.Access-Control-Request-Headers
헤더는 실제 요청이 전송될 때 사용자 정의 헤더X-PINGOTHER
와Content-Type
을 사용할 것임을 서버에게 알린다.
서버가 응답한 헤더를 보자.
Access-Control-Allow-Origin: https://foo.example
헤더로 응답하여 요청을 보낸 출처 도메인만 접근 가능하도록 제한한다.Access-Control-Allow-Methods
헤더로 응답하여POST
와GET
메서드가 해당 리소스를 요청하는 데 유효한 메서드임을 나타낸다.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));

추가로 서버에서 설정하는 헤더에 몇 가지 제약이 생긴다.
- 응답 헤더의
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을 구분해 보면 다음과 같다.
- scheme(프로토콜) : https
- host : www.example.com
- port : 433
여기서 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
- 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
- https://developer.mozilla.org/ko/docs/Web/HTTP/CORS
- https://developer.mozilla.org/ko/docs/Web/Security/Same-origin_policy
- https://spring.io/guides/gs/rest-service-cors
- https://docs.spring.io/spring-security/reference/reactive/integrations/cors.html
- https://web.dev/articles/fetch-metadata?hl=ko
- https://www.w3.org/TR/fetch-metadata/
- https://web.dev/articles/same-site-same-origin?hl=k
'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 |
- Total
- Today
- Yesterday
- Computer_Networking_A_Top-Down_Approach
- 파이썬 for Beginner 솔루션
- Do it! 정직하게 코딩하며 배우는 딥러닝 입문
- 쉽게 배우는 운영체제
- 패킷 스위칭
- Spring Data JPA
- git
- git merge
- 스프링 컨테이너
- Gradle
- Spring
- 스프링 테스트
- 프로그래머스
- git branch
- 운영체제 반효경
- Python Cookbook
- fetch join
- Thymeleaf
- 선형 회귀
- 스프링
- 파이썬 for Beginner 연습문제
- 쉘 코드
- 스프링 mvc
- 지옥에서 온 git
- 김영환
- 생활코딩 javascript
- jsp
- Spring Boot
- 방명록 프로젝트
- JPA
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |