CORS (Cross-Origin Resource Sharing)

박영준·2022년 12월 18일
0

Spring

목록 보기
50/58

CORS 에 대한 설명 은 개발자 면접에서 자주 나오는 질문 중의 하나!

1. 정의

  • "Cross-Origin Resource Sharing", "교차 출처 리소스 공유", "다른 출처 간의 자원을 공유하는 정책"

  • 다른 출처(도메인, 프로토콜, 포트) 에서 실행 중인 웹 어플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하는 웹 보안 메커니즘

  • 한 출처에 있는 자원에서 다른 출처에 있는 자원에 접근하도록 하는 개념

    • 웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때, 교차 출처 HTTP 요청을 실행

    • browser에서는 보안적인 이유로 cross-origin HTTP 요청들을 제한하기 때문에,
      cross-origin 요청을 하기 위해서는 서버의 동의가 필요하다.

      • 'CORS' 는 browser에서 추가 HTTP-header를 이용하여, cross-origin 요청을 안전하게 할 수 있도록 하는 메커니즘

    cross-origin
    cross-origin 은 다음 中 한 가지라도 다른 경우를 말한다.

    • 프로토콜 - http와 https는 프로토콜이 다르다.
    • 도메인 - domain.com과 other-domain.com은 다르다.
    • 포트 번호 - 8080포트와 3000포트는 다르다.

    Origin (출처)

    1. 정의

    Protocol + Host + Port 3가지가 같으면, 동일 출처(Origin)라고 한다.

    1. 예시

2. 필요성

1) CORS 가 없어, 모든 곳에서 데이터를 요청할 수 있게 된다면?

기존 사이트와 완전히 동일하게 동작하도록 하여, 사용자가 로그인을 하도록 만들고
로그인했던 세션을 탈취하여, 악의적으로 정보 추출 or 타인의 정보를 입력하는 등.. 공격이 가능하다.
→ 다른 사이트에서 원래 사이트를 흉내낼 수 있게 된다.

2) CORS를 사용할 경우?

  • 위와 같은 공격을 할 수 없도록, browser에서 보호
  • 필요한 경우에만 서버와 협의하여 요청할 수 있도록 한다.
  • 다른 출처 리소스에 접근성을 높일 수 있다.

3. SOP(동일 출처 정책) vs CORS(다른 출처 정책)

1) 비교

동일 출처

  • '요청하는 클라이언트'와 '요청받는 서버'가 같은 출처에 있으면 동일 출처 요청 (동일 출처 정책, Same-Origin Policy)

    • 자신과 동일한 도메인만 서버로부터 데이터를 요청하여 받을 수 있도록 하는 정책
  • domain-a.com 유저가 domain-a.com 서버에 요청하면, 동일 정책이기 때문에 아무런 문제 X

  • (도메인 이외에) 자원

    • 같은 프로젝트 내에 정의된 css 파일 요청 : 동일 출처 요청

다른 출처

  • '요청하는 클라이언트'와 '요청받는 서버'가 서로 다른 서버에 있으면 다른 출처 요청 (다른 출처 정책, Cross-Origin Policy)

  • domain-a.com 유저가 domain-b.com 서버에 요청하면, 호스트(Host)가 다르기 때문에 다른 출처 요청

  • (도메인 이외에) 자원

    • 다른 외부 사이트에서 실시간으로 import를 통해 가져오는 font : 다른 출처 요청

2) 예시

https://domain-a.com 의 프론트 엔드 JavaScript 코드가 XMLHttpRequest 를 사용하여 https://domain-b.com/data.json 을 요청하는 경우(즉, 다른 출처)

  • 보안 상의 이유로, 브라우저는 스크립트에서 시작한 교차 출처 HTTP 요청을 제한
  • 다른 출처의 리소스를 불러오려면 그 출처에서 올바른 CORS 헤더를 포함한 응답을 반환해야 한다.

4. CORS 동작 방식

단순요청(Simple Request), 프리플라이트 요청(Preflighted Request), 인증정보요청(Credential Request)
(실제 요청이 실행되기 이전에 검사를 하고 허용할지말지를 결정할 수 있는 프리플라이트 요청을 권장한다)

1) Simple Request (단순 요청)

  1. 브라우저는 다른 출처에 자신의 주소 https://www.site.com 를 origin에 담아서 서버로 요청을 보낸다.

  2. 서버는 요청을 확인하고, 다른 출처 주소 https://www.site.com에 에 접근이 가능하다는 access-control-allow-origin 에 해당 주소(https://www.site.com)를 담아서 결과를 리턴

  3. 유효한 요청이라면, 리소스를 응답
    유효하지 않은 요청이라면, 브라우저에서 이를 막고 에러가 발생

    • '유효하지 않은 요청' 이란?
      • 서버가 이 헤더에 응답하지 않을 경우
      • 헤더 값이 요청의 출처와 일치하지 않는 도메인인 경우
      • 요청한 출처가 서버의 access-conrol-allow-origin 에 포함되어 있는 경우

    access-control-allow-origin

    • CORS 헤더의 중요 요소 중 하나
    • 어떤 요청을 허용할지 결정
    • 이 헤더 값은 하나의 출처가 될 수도 있고, "*"를 사용해 어떤 출처도 허용하도록 할 수 있다.

(1) 정의

예비 요청을 보내지 않고 서버에게 바로 본 요청을 보낸 후,
응답 헤더의 Access-Control-Allow-Origin 값을 확인하여 CORS 정책 위반 여부를 확인

(2) 사용 조건

  1. 요청의 메소드는 GET, HEAD, POST 중 하나여야 한다.

  2. Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width 를
    제외한 헤더를 사용하면 안 된다.

  3. 만약 Content-Type를 사용하는 경우,
    application/x-www-form-urlencoded, multipart/form-data, text/plain 만 허용된다.

2) Preflight Request (예비 요청)

  1. OPTIONS 를 사용해 자신의 주소 https://www.api.com?q=test 와 origin, access-control-request-method, access-control-request-headers 를 같이 보낸다.

    • 내용물은 없이 헤더만 전송
    • 실제 요청을 보내는 것이 안전한지 확인하기 위해, 먼저 OPTIONS 메서드를 사용하여 cross-origin HTTP 요청을 보낸다.
      → 사용자 데이터에 영향을 미칠 수 있는 요청이므로 사전 확인을 하기 위함이다. (예비 요청의 응답이 성공하지 못하면, CORS 정책 위반 이슈가 발생)
  2. 브라우저가 서버에서 응답한 헤더를 보고, 유효한 요청인지 확인

  3. 유효한 요청이라면, 정상적인 응답으로 access-control-allow-origin, access-control-allow-method, access-control-allow-headers, access-control-max-age 를 응답받는다.
    유효하지 않은 요청이라면, 요청은 중단되고 에러가 발생

    요청 헤더 목록
    Access-Control-Request-Headers
    Preflight Request 시, 실제 요청에서 어떤 header 를 사용할 것인지 서버에게 알리기 위해 사용

    Access-Control-Request-Method
    Preflight Request 시, 실제 요청에서 어떤 method 를 사용할 것인지 서버에게 알리기 위해 사용

    Origin
    어디서 요청을 했는지 서버에 알려주는 주소

    응답 헤더 목록
    Access-Control-Allow-Origin
    서버가 허용하는 출처

    Access-Control-Allow-Methods
    서버가 허용하는 HTTP 메서드 리스트

    Access-Control-Allow-Headers
    서버가 허용하는 header 리스트

    Access-Control-Max-Age
    프리 플라이트 요청의 응답을 캐시에 저장하는 시간

     

(1) 정의

  • 일반적으로 사용하는 방식

  • 브라우저는 요청을 한 번에 보내지 않고 예비 요청과 본 요청으로 나누어서 서버로 전송

    • 이때 본 요청을 보내기 전에, 예비 요청은 OPTIONS 메소드를 사용하여 브라우저 스스로 이 요청이 안전한지 확인하는 역할을 한다.

3) Credential Request (인증정보 요청)

서버 응답에 access-control-allow-credentials 가 true로 설정되지 않았거나 access-control-allow-origin 헤더에 있는 값이 허용된 출처가 아니라면 오류가 발생

응답 헤더 목록
Access-Control-Allow-Credentials
- Credentials가 true 일 때, 요청에 대한 응답이 노출될 수 있는지를 나타낸다.
- Preflight Request 에 대한 응답의 일부로 사용될 경우, 실제 자격 증명을 사용하여 실제 요청을 수행할 수 있는지를 나타낸다.
- 간단한 GET 요청은 preflight 되지 않으므로, 자격 증명이 있는 리소스를 요청할 때, 헤더가 리소스와 함께 반환되지 않으면 브라우저에서 응답을 무시하고 웹 콘텐츠로 반환하지 않는다.

5. cross-origin 설정하기

CORS 에러가 발생한 상황

spring Boot에서 cross-origin 을 설정하는 방법에는 4가지가 있다.

1) @CrossOrigin 설정

(1) Method 에 @CrossOrigin 설정

@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin		// cross-origin 설정
    @RequestMapping(method = RequestMethod.GET, path = "/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }

    @RequestMapping(method = RequestMethod.DELETE, path = "/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}
  • retrieve() 메서드에 선언된 @CrossOrigin의 기본 설정
    1. 모든 출처가 허용된다.
    2. 허용된 HTTP 메서드는 @RequestMapping에 선언된 메서드들
    3. 프리플라이트 응답은 30분 동안 캐시된다.

(2) Controller 에 @CrossOrigin 설정

예시 1

@CrossOrigin(origins = "http://example.com", maxAge = 3600)		// cross-origin 설정
@RestController
@RequestMapping("/account")
public class AccountController {

	// 메서드 1
    @RequestMapping(method = RequestMethod.GET, path = "/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }

	// 메서드 2
    @RequestMapping(method = RequestMethod.DELETE, path = "/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}
  • 컨트롤러에 설정했으므로, AccountController에 있는 retrieve() 와 remove() 함수 모두에 적용된다.

예시 2

@CrossOrigin(originPatterns = "http://localhost:8080")
@RestController
public class LoginController {

    @GetMapping("/login")
    public String login() {
        return "로그인 성공! ID: jayon, PW: 1234";
    }
}
  • originPatters 속성을 통해 요청을 허용할 출처를 적어준다.

(3) 개별적으로 각각 @CrossOrigin 적용

@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin("http://example.com")
    @RequestMapping(method = RequestMethod.GET, "/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }

    @RequestMapping(method = RequestMethod.DELETE, path = "/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}
  • Spring 에서 여러가지 CORS 정책을 복합해서 설정 할 수 있다.

  • retrieve() 메서드

    • 허용 출처가 "http://example.com" 밖에 안된다.
    • 그러나, 별도의 설정이 없으므로 모든 출처가 가능
      (모든 메서서드들은 3600초가 캐시 시간)

2) 전역 설정

(1) WebMvcConfigurer

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:8080")
                .allowedMethods(HttpMethod.GET.name())
                .maxAge(3000);
    }
}
  • CORS 정책의 설정은 WebMvcConfigurer 를 구현하여 설정할 수 있다.
    → 이는 필터 기반이기 때문에, 전역적으로 모든 요청에 대해서 검사한다.

  • addCorsMappings() 를 재정의 하면된다.

    • addMapping() : CORS 정책을 적용할 URL 패턴을 설정

    • allowedOrigins()

    • allowedMethods()

      • GET, POST와 같은 HTTP 메소드의 종류도 제한할 수 있다.
      • allowedMethods("GET", "POST")
    • maxAge(3000)

      • 원하는 시간만큼 pre-flight 리퀘스트를 캐싱 해둘 수도 있다.

(2) filter

filter 를 생성하는 방법이다.
만약, security 를 사용한다면 해당 필터를 security 에 추가해줘야 한다.

CorsConfig 클래스

@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**",config);
        return new CorsFilter(source);
    }
}
  • 만약 credentialed request가 아닐 경우, allowCredentials(true) 부분을 제거해야한다.

SecurityConfig 클래스

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtTokenProvider jwtTokenProvider;
    private final CorsConfig corsConfig;

	...
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .httpBasic().disable()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()

                .authorizeRequests()
                .antMatchers("/*/login", "/*/login/**", "/*/signup", "/*/signup/**").permitAll()
                .anyRequest().hasRole("USER")

                .and()
                .addFilter(corsConfig.corsFilter())			// ** CorsFilter 등록 **
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), 		// Authentication Filter 인증
                		UsernamePasswordAuthenticationFilter.class);
        return httpSecurity.build();
    }
   ...
}   
  • Spring Security 사용 시, CORS 설정을 하기 위해서 Authentication Filter 인증보다 앞에 필터를 추가해주어야 한다.

  • 만약 credentialed request가 아닐 경우, config.setAllowCredentials(true) 부분을 제거해야한다

    사용 예시

    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class WebSecurityConfig {
        private final CorsConfig corsConfig;
        private final JwtUtil jwtUtil;
        private final UserDetailsServiceImpl userDetailsService;
        private final AuthenticationConfiguration authenticationConfiguration;
    
        ...
    
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http.csrf((csrf) -> csrf.disable());
    
            // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
            http.sessionManagement((sessionManagement) ->
                    sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            );
    
            http.authorizeHttpRequests((authorizeHttpRequests) ->
                            authorizeHttpRequests
                                    .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                                    .requestMatchers("/").permitAll()
                                    .requestMatchers("/api/auth/**").permitAll()
                                    .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                                    .requestMatchers(HttpMethod.GET, "/api/homepost").permitAll()
                                    .requestMatchers(HttpMethod.GET, "/api/post/{id}/**").permitAll()
                                    .anyRequest().authenticated()
            );
    
            // 필터 관리
            http.addFilterBefore(corsConfig.corsFilter(), UsernamePasswordAuthenticationFilter.class);		// corsFilter 추가
            http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
            http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    
            return http.build();
        }
    }

    내가 참가했던 프로젝트에서 사용한 예시를 가져왔다.

3) 프록시 서버 사용

해당 방법은 클라이언트에서 프록시 서버를 필요로 할 때 사용하면 좋다.

@Controller
public class UserController {

    @GetMapping("/api/view")
    public String view() {
        return "/cors";
    }

    @GetMapping("/api/proxy")		//  localhost:8080/api/proxy 로 요청을 보내면, CORS 이슈 없이 잘 응답이 렌더링된 것을 확인할 수 있다.
    @ResponseBody
    public String proxyView() {
        String url = "http://localhost:1000/login";

        RestTemplate restTemplate = new RestTemplate();
        return restTemplate.getForObject(url, String.class);
    }
}
  • 서버 단은 CORS 정책이 위반될 때, 200번 코드를 반환한다.(400, 500번대와 같은 상태 코드가 아닌)

    • 따라서, 브라우저 단에서 서버의 Access-Control-Allow-Origin 값을 보고 방금 보낸 요청의 출처가 허용되는지 판단하고, 허용되지 않으면 CORS 이슈가 발생한다.
      즉, 요청을 보내는 쪽(브라우저)에서 프록시 서버를 만들어 간접적으로 전달하면 응답을 받을 수 있는 것이다.
  • RestTemplate를 통해 서버와 서버 간 통신을 했다.

    • 서버 : CORS 이슈 없이 응답 데이터를 얻어올 수 있다.
    • 브라우저 : 요청을 보낸 출처와 응답을 받은 출처가 같으므로, 응답 데이터를 정상적으로 렌더링 해준다.

6. CORS 우회 방법

1) JsonP (Json with Padding)

(1) 원리

  • 웹 브라우저에서 리소스 파일들(css, js 등...)은 동일 출처 정책(SOP)에 영향을 받지 않고 로딩이 가능하다.

    • 이 점을 응용해서, 외부 서버에서 읽어온 js 파일을 json 으로 바꿔주는 일종의 편법적인 방법이다.
  • 서버에 <script> 를 호출하여 실행시키는 우회방식 중 하나

단, 리소스 파일을 GET 메서드로 읽어오기 때문에, GET 방식의 API만 요청이 가능!

(2) 방법

클라이언트

$.ajax({
	url: 'http://localhost:8080/hello?callback=?',
	dataType:'jsonp',
    jsonpCallback: 'myCallback',
	success: function(res) {
		console.log(res);
	}
});

서버

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice;

@ControllerAdvice
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
	public JsonpAdvice() {
		super("callback");
	}}
}
  • AbstractJsonpResponseBodyAdvice 클래스 를 사용

  • @ControllerAdvice 어노테이션과 조합하여 구현

2) CRLF Injection

(1) 원리

일반적인 헤더 정보 형태

CRLF Injection 한 헤더 정보 형태

헤더의 정보를 우리가 임의로 수정할 수 있다고 할 떄
CR(Carriage Return)과 LF(Line Feed)를 삽입해서, 아래에 있는 헤더 정보를 header body 로 보내 header정보를 무효화 시키는 것이다.

(CRLF : 줄 바꿈)


참고: CORS란 무엇인가?
참고: [Spring] Spring에서 CORS 이슈를 해결하는 방법
참고: [Spring Boot] CORS 설정하기
참고: [이슈해결] CORS with Spring (MVC, Security)
참고: 동일출처정책과 CORS 그리고 해결 방법
참고: 스프링 부트에서 JSONP를 다루는 방법, CORS 이슈 해결하기
참고: SOP, CORS, CSP 개념과 우회방법(Concept & Bypass)

profile
개발자로 거듭나기!

0개의 댓글