
최근 Spring Security를 공부하고 있는데, 보안 관련 주요 토픽으로 CORS, CSRF가 나왔다.
CORS, CSRF의 개념은 웹 보안 측면에서 중요하다고 생각해서 내용을 정리해보려고 한다.
CORS(Cross-Origin Recource Sharing)
CORS는 프로토콜인데, 서로 다른 origin인 경우 리소스와 상호 작용하기 위해 클라이언트인 브라우저에서 실행되는 스크립트이다.
프로토콜(Protocol)?
컴퓨터 내부에서, 또는 컴퓨터 사이에서 데이터의 교환 방식을 정의하는 규칙 체계
예를 들어 UI 앱에서 서로 다른 도메인인 API를 호출할 시 CORS로 인해 기본적으로 차단된다. 이는 대부분의 브라우저에서 구현되는 W3C의 스펙이다.
W3C?
World Wide Web Consortium
웹 기술의 표준을 정의하는 공식 기관
따라서 CORS는 보안이나 공격과 같은 문제가 아니라 서로 다른 Origin 간의 데이터 및 통신을 할 때 브라우저에서 이를 방지하기 위해 제공하는 기본 보호 기능이다.
예를 들어 백엔드 서버와 프론트엔드 서버의 IP가 서로 다르다고 해보자.
이때 FE 서버에서 클라이언트가 로그인을 했을 때, BE의 로그인 API가 호출될 것이다. 이때 BE에서 FE 도메인을 CORS 허용 설정하지 않으면 접근이 차단된다. (default 설정)
JSP, Thymeleaf와 같은 BE, FE가 같이 구동되는 SSR(Server Side Rendering)의 경우, CORS 설정은 신경쓰지 않아도 될 것이다.

그림을 보자. hello.world.com 출처에서 by.world.com이란 다른 출처의 api를 호출했다. 이때 CORS를 허용하지 않으면 api를 통해 리소스에 접근할 수 없다.
따라서 BE에서 CORS를 허용해야 한다.
@CrossOrigin()은 CORS를 허용할 때 클래스, 메소드에 작성하는 어노테이션이다.
@CrossOrigin(origins = "도메인")
@CrossOrigin(origins = "*")
그러나 CORS를 허용하려고 하는 클래스, 메서드마다 어노테이션을 작성하는 것은 번잡하고 귀찮은 일이다.
개발자는 반복 작업, 귀찮은 일을 싫어한다. 이런 경우 구성 정보를 설정하는 것이 좋다.
SecurityFilterChain을 스프링 빈으로 등록할 때 CORS 관련된 설정을 할 수 있다.
@Configuration
public class ProjectSecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("http://hello.world.com"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setAllowCredentials(true);
config.setAllowedHeaders(Collections.singletonList("*"));
config.setMaxAge(3600L); //1시간
return config;
}
}))
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/myHome", "/myAccount", "/user").authenticated()
.requestMatchers("/register").permitAll())
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.build();
}
...
}
코드가 길어서 복잡해보이지만 사실 간단한 코드이다.

CORS 정리
CORS(Cross-Origin Recource Sharing)는 보안 또는 해커의 공격과 관련된 것이 아닌, 서로 다른 도메인에서 데이터를 통신하려고 할 때 브라우저에서 이를 중지하기 위해 발생하는 기본 보호 기능이다.
CSRF(Cross-Site Request Forgery)"
CSRF란 명시적인 동의 없이 사용자를 대신하여 웹 어플리케이션에서 악의적인 행동을 취하는 공격이다.
일반적으로 사용자의 신원을 직접적으로 도용하지는 않지만, 사용자의 의지와 상관없이 악의적인 행동을 취한다.
CSRF의 간단한 예시로 근래 많이 발생하고 있는 문자 보이스피싱이 있다. 스팸 문자의 링크를 클릭하면 순식간에 개인 정보가 모두 유출된다.
좀 더 명확한 예시를 들어보자.
상민이가 웹 사이트 coupang.com과 hacker.com을 사용하고 있다고 가정하자.
상민이가 coupang.com에 로그인할 때 BE 서버에서는 브라우저에 저장할 쿠키(Cookie)를 준다.

이때 상민이는 hacker.com 웹사이트에 접속했다.

이때 hacker.com은 상민이의 쿠팡 계정을 해킹하기 위해 링크가 삽입 되어있는 웹 페이지를 반환한다.
상민이가 웹 페이지의 광고에 혹해 이를 클릭한다면, 순식간에 coupang.com에 악의적인 요청이 간다.
쿠팡의 BE 서버는 로그인 쿠키가 이미 동일한 브라우저에서 존재하고 있고, 해커의 계정 정보 변경과 같은 악의적인 요청이 동일한 coupang.com이란 도메인에서 이루어지고 있기 때문에, 상민이의 요청인지 해커의 요청인지 구별할 수 없다.

CSRF 공격을 처리하기 위해서는, 해당 HTTP 요청이 UI를 통해 이루어졌는지 확인해야 한다.
이때 등장하는 것인 CSRF Token이다. CSRF Token은 CSRF 공격을 방지하는 데 사용되는 임의의 토큰이다. 토큰은 사용자 세션마다 고유해야 하며, 쉽게 추측할 수 없도록 긴 랜덤의 값이어야 한다.
CSRF Token을 적용하고 다시 상민이가 두 개의 웹사이트에 접속한다고 하자.
상민이가 coupang.com에 로그인할 때 BE 서버는 브라우저에 저장할 쿠키를 준다.
해당 쿠키에는 특정 유저 세션을 위해 임의로 생성된 고유한 CSRF 토큰이 저장된다. CSRF 토큰은 세션 쿠키에 노출되지 않도록 HTML 폼에 숨겨진 매개변수 내에 삽입된다.

이때 상민이는 hacker.com에 접속한다.

상민이가 또 다시 광고에 혹해 링크를 클릭하면, coupang.com에 악의적인 요청이 간다.
쿠팡의 BE 서버는 해당 요청이 상민이의 요청인지 해커의 요청인지 구별할 수 없다.
따라서 BE 서버는 Authentication 쿠키 외에 CSRF Token이 담긴 쿠키를 확인한다. 이 쿠키는 최초로 생성될 때와 동일해야 한다.

CSRF Token은 최종 클라이언트의 요청이 동일한 UI에서 오는지 확인하기 위해 어플리케이션 서버에서 사용된다. 어플리케이션 서버는 CSRF Token이 일치하지 않으면 요청을 거부한다.
해커가 JS를 실행할 때에는 hacker.com이란 도메인에서 실행된다. 상민이가 로그인할 때 쿠키에 저장된 CSRF Token은 coupang.com 도메인에 저장된다.
이러한 제약 때문에 해커가 아무리 hacker.com 도메인에서 해킹을 하기 위해 JS를 실행해도 쿠팡의 BE 서버에서는 CSRF Token이 있어야 로직을 수행할 수 있기에 해킹 시도는 실패한다.
@Configuration
public class ProjectSecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("http://localhost:9090"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setAllowCredentials(true);
config.setAllowedHeaders(Collections.singletonList("*"));
config.setMaxAge(3600L); //1시간
return config;
}
}))
.csrf(csrf -> csrf
.csrfTokenRequestHandler(requestHandler)
.ignoringRequestMatchers("/contact", "/register")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
/* BasicAuthenticationFilter 이후에 CsrfCookieFilter를 실행한다.
BasicAuthenticationFilter는 로그인 이후에 동작하는 필터*/
).addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/myHome", "/myAccount", "/user").authenticated()
.requestMatchers("/register").permitAll())
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.build();
}
}
CSRF 관련 설정 람다가 추가됐다.
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
이는 FE, BE 서버가 다를 때, FE에서 JS로 BE의 쿠키를 읽으려고 할 때 이를 허용하기 위한 설정이다.
CsrfCookieFilter
@Slf4j
public class CsrfCookieFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
/**
* CsrfToken을 헤더에 보내면, Spring Security는 해당 토큰과 동일한 쿠키를 생성해서 반환한다.
*/
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if(csrfToken.getHeaderName() != null){
log.info("csrfToken.getHeaderName() = {}, csrfToken.getToken() = {}", csrfToken.getHeaderName(), csrfToken.getToken());
response.setHeader(csrfToken.getHeaderName(), csrfToken.getToken());
}
filterChain.doFilter(request, response);
}
}
필터에서는 먼저 사용 가능한 HttpServletRequest에서 CSRF 토큰을 읽는다. 그 다음 Security가 CSRF 토큰을 생성한 경우 Null이 아니라면 응답 헤더에 헤더의 이름과 값을 채운다. 이후에는 필터 체인을 호출하는데 이렇게 한다면 응답을 보낼 때마다 HTTP 헤더에 CSRF 토큰 값이 표시된다.
이 로직에서 응답 헤더만 보냈고, CSRF 토큰 쿠키는 생성하지 않는다.
Security는 개발자가 응답 헤어뎅 CsrfToken 값을 채울 때마다 CSRF 쿠키를 자동으로 만들어준다.
출처
https://jaykaybaek.tistory.com/29
https://www.w3.org/WAI/standards-guidelines/ko
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS