본문 바로가기

DEVELOPER/Spring & JPA

[보안/인증] CSRF 로 인해서, 403에러가 발생했을 때


안녕하세요😎 백엔드 개발자 제임스입니다 :)

오늘 포스팅은 제가 겪었던 에러와 이를 어떻게 해결했는지 공유하려고 합니다. 에러의 원인은 CSRF로 인한 이유였습니다. API 서비스 개발 후 테스트를 진행하던 도중 발생했는데요. 브라우저에 URL을 입력하여, 뷰 테스트를 진행했을 때는 에러가 발생하지 않았습니다. 반면, Test파일에서 기능 테스트를 진행하면, 403 에러가 발생했습니다.

테스트 실행 시 에러 발생

위 그림을 보다시피, Status가 3XX(Redirection)이 나와야 하는데 403(Forbidden)이 나온 것을 볼 수 있습니다. 이 외에도 Status 200(OK)을 기대했는데 에러가 발생할 수 있습니다.


원인

분명 브라우저에서 URL을 통해 Request를 보내면 제대로 수행되는데요. 왜 IDE에서 테스트를 진행하면 오류가 나는 것일까요?

문제의 원인은 우리가 사용한 스프링 시큐리티에 있습니다. 스프링 시큐리티(Spring Security)의 애너테이션인 @EnableWebSecurity에는 기본적으로 CSRF 공격을 방지하는 기능을 지원합니다.

CSRF(Cross-Site Request Forgery)란?
사이트 간 요청 위조라는 의미로, 다른 오리진을 가진 사이트에서 form 요청을 보내는 것입니다. 
자세하게는 일반 사용자가 악의적인 공격자에 의해 '등록, 수정, 삭제' 등의 행위를 특정 웹사이트에 요청하도록 만드는 공격입니다.

위 같은 내용을 CSRF 공격이라고 합니다. 이를 방어하기 위해선 다양한 방법이 있습니다. 대표적으로 Refferer 검증, CSRF Token, Double Submit Cookie 검증 방법이 있습니다. 이중 CSRF Token 방식이 활용됩니다.

다시 본론으로 돌아오겠습니다. 즉, Spring Security가 적용되면서 CSRF 공격 방어가 활성화된 것이고, 이로 인해 IDE에서 진행한 테스트 과정을 다른 사이트의 요청으로 인지한 것입니다.

하지만 분명 내가 작성하는 코드인데 어떻게 다른 사이트로 인지한 것일까요? 자세한 내용은 아래에서 알아보겠스니다.


원리

위에서 언급한 원인은 요약한 내용입니다. 한번 상황을 자세하게 보도록 하겠습니다. 

저는 웹 애플리케이션을 개발하기 위해 Thymeleaf(타임리프)를 사용했습니다. 타임리프 템플릿은 CSRF 토큰 기능을 지원합니다. 따라서 IDE에서 진행한 테스트에서는 토큰이 없었기 때문에, 403 에러가 발생한 것입니다.

위 이미지는 Thymeleaf를 사용하여 만든 웹페이지입니다. 해당 뷰에서 가입하기(submit)를 요청할 경우에는 정상적으로 수행합니다. 그 이유를 보기 위해서 개발자 환경을 보도록 하겠습니다.

form 태그 아래에 hidden 속성 input 박스에 들어있는 값이 csrf 토큰값입니다. 위에서 언급한 대로 Thymeleaf에서 지원하는 임의의 토큰입니다. 사용자가 form 요청을 보내면, 스프링 시큐리티에서 토큰 값을 확인한 후 정상적인 요청인지 판단합니다. 이때 CSRF 토큰이 존재하지 않거나, 기존 토큰과 일치하지 않았을 때, 4XX 상태 코드를 리턴하는 것입니다.


해결

위 문제를 해결할 방법은 두 가지 있습니다. 

1) 스프링 시큐리티의 csrf 방지 기능을 비활성화하기

스프링 시큐리티의 애너테이션 @EnableWebSecurity가 적용된 Configuration에서 아래 명령어를 통해 비활성화합니다.

http.csrf().disable();
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
    
        http.csrf().disable(); // csrf 방지 기능 비활성화
    }

 하지만 실제로 애플리케이션을 배포할 경우엔 이 방법은 권장하지 않습니다.

Spring 공식 문서
"When to use CSRF protection"
Our recommendation is to use CSRF protection for any request that could be processed by a browser by normal users. If you are only creating a service that is used by non-browser clients, you will likely want to disable CSRF protection.

-> 일반 사용자가 브라우저를 통해 처리할 수 있는 모든 요청에 대해 CSRF 보호를 사용하는 것을 권장합니다.
만약, 브라우저가 아닌 클라이언트에서 사용하는 서비스만 생성하는 경우 CSRF 보호를 비활성화할 수 있습니다.

2) 테스트 시에 csrf() 메서드 추가하기

    @Test
    @DisplayName("회원 가입 처리 - 입력값 정상")
    public void signUpSubmit_with_correct_input() throws Exception {
        mockMvc.perform(post("/sign-up")
                        .param("nickname","james")
                        .param("email","test@test.com")
                        .param("password","12345678")
                        .with(csrf())
                )
                .andExpect(status().is3xxRedirection())
                .andExpect(view().name("redirect:/"));
    }

위와 같이 MockMvc 클래스를 사용하여 테스트할 때, with(csrf())를 추가합니다.

csrf() 메서드의 클래스는 SecurityMockMvcRequestPostProcessors입니다.
반응형