스프링 시큐리티를 이용하면 기본적으로 csrf() 옵션이 설정된다.
그렇기 때문에 GET Method를 제외한 POST, DELETE 등이 제대로 실행되지 않고 403 Forbidden 에러를 내뱉을 수 있다.
위 에러는 아래 테스트코드에서 발생했다.
여기서 csrf 공격이란, 간단히 말해서 아래와 같다.
공격자가 임의로 이미지나 하이퍼링크에 자신이 만든 악성링크를 첨부하거나, 이메일로 전송한다.
사용자는 실수로 해당 링크를 클릭한다. 이 때, 사용자는 인증이 완료된 로그인된 상태여야 한다.
공격자는 사용자가 링크를 클릭함으로써 JSESSIONID 등 권한을 탈취하여 마음껏 크래킹할 수 있는 상태가 된다.
이러한 공격을 csrf 공격이라고 한다.
왜 테스트 코드에서 403 Forbidden이 뜬걸까?
먼저, 이 프로젝트의 경우 실제 실행은 전혀 문제가 없다. 단지 테스트 코드에서만 403 Forbidden이 발생하는 경우이다.
그렇다면 왜 테스트 코드에서만 403 Forbidden이 나오면서 깨지는걸까?
정답은 바로 Thymeleaf 때문이다.
현재 이 프로젝트는 Java Spring boot + Thymeleaf 기술스택으로 운영되고 있다. (곧 kotlin + js로 마이그레이션 예정이긴 하다.)
타임리프 템플릿은 기본적으로 스프링 시큐리티의 csrfToken을 제공해준다.
실제로 타임리프 환경에서는 _csrf 를 hidden 타입으로 보내주는 것을 확인할 수 있다.
하지만 테스트 코드를 작동할 때에는 별다른 csrfToken을 제공할 방법이 없기 때문에 위와 같은 에러가 뜨는 것이다.
어떻게 하면 위 에러를 테스트 코드에서도 보이지 않게 할 수 있을까?
아래와 같은 방법들이 있다.
1. [비추천] @AutoConfigureMockMvc(addFilters=false) 또는 csrf().disable()
아예 mockmvc를 사용할 때 필터의 기능을 제외시켜버리거나, csrf 체크를 해제시켜버리는 것이다.
@AutoConfigureMockMvc(addFilters=false) 옵션으로 세팅해두면 스프링 시큐리티의 필터가 동작하지 않기 때문에 보안 관련 체크를 거의 제외시킬 수 있다. 따라서 csrf 체크를 진행하지 않기 때문에 테스트가 통과할 것이다.
하지만 상당히 위험한 방법이므로 비추천한다.
또는 위와 같이 csrf().disable()로 세팅하는 방법도 있다.
스프링 시큐리티에서 제공해주는 csrf() 옵션을 해제하는 것이다.
필터 자체를 제외시켜버리는 addFilters=false 옵션보다는 괜찮아보인다.
또, 2020년 2월 경부터 Chrome 80 버전에서는 cookie의 samesite 옵션의 디폴트가 none에서 lax로 바뀌기도 해서 위와 같이 설정해도 csrf 공격의 대부분은 막을 수 있어보인다.
여기서 samesite=lax 옵션이란, 같은 웹사이트의 요청 및 navigation과 같은 리다이렉트, 안전한 Http Method 요청일 경우 전송을 허용한다는 의미이다. csrf 공격은 크래커가 임의로 심어둔 악성링크로 인해 발생하는 경우이므로, 같은 웹사이트 및 navigation 이동에 해당되지 않기 때문에 samesite=lax 옵션으로 막을 수 있다. 더 자세한 내용은 아래 링크를 참고하자.
하지만 samesite=lax 옵션이 만능은 아니다.
referer 체크 등이 이루어지지 않고 단순 Http Method나 navigation에 보안을 의존하는 것은 비교적 허술할 수 있다고 판단했기 때문.
따라서 더 안전한 방법을 찾아보도록 했다.
2. Session-Cookie 방법 대신 JWT 사용
JWT를 사용할 경우, api 송수신으로 ajax 처리할 때에 authorization token을 넣어준다.
공격자는 단순히 악성링크를 첨부하는 과정만 거치므로 authorization token을 따로 심어줄 순 없기 때문에 JWT 환경에서는 csrf 공격을 예방할 수 있다.
실제로 No Cookies, No CSRF 라는 말이 괜히 있는게 아니다.
https://security.stackexchange.com/questions/62080/is-csrf-possible-if-i-dont-even-use-cookies
하지만 현재 이 프로젝트에서는 spring security를 사용한 인증 인가 과정을 거치고 있으며, spring security는 기본적으로 세션-쿠키 방식으로 처리되고 있다.
기술스택을 갑자기 바꾸기에는 리스크가 너무 크기 때문에 다른 방법이 없을까 추가로 찾아보았다.
3. spring-security-test에서 제공해주는 csrf() 옵션
애초에 현재 문제상황이 실행은 문제없이 잘 되는데, 테스트에서 csrfToken을 제공받지 못하기 때문에 발생한 것이었다.
그렇기 때문에 테스트 환경에서 csrfToken을 제공받으면 해결되는 문제!
build.gradle에 아래 의존성을 추가해주자.
dependencies {
testImplementation 'org.springframework.security:spring-security-test'
}
그리고 테스트 코드에 아래와 같이 추가해주자.
위 옵션은 아래와 같이 소개돼있다.
Creates a RequestPostProcessor that will automatically populate a valid CsrfToken in the request.
Returns: the SecurityMockMvcRequestPostProcessors.CsrfRequestPostProcessor for further customizations.
MockMvc에서 request에 자동으로 valid한 CsrfToken을 제공해주는 셈.
위와 같이 세팅해주면 수많은 구글링 결과에서 안내해주는 addFilters=false 옵션이나 csrf().disable() 설정 없이 테스트를 통과할 수 있을 것이다.
사실 결론이 되게 허무하긴 하다. 단순 with(csrf()) 옵션을 추가해주면 끝나는 문제라니.
하지만 덕분에 session-cookie와 jwt에 대해서 다시 한 번 복습할 수 있게 된 계기가 됐다.
또, cookie의 samesite 옵션에 대해서 공부할 수 있었다.
흠, 근데 이 프로젝트를 진행하면서 드는 생각은... 스프링 시큐리티도 좋지만 그냥 필터, 인터셉터 직접 구현해서 만드는 게 더 나은 거 같다.
도움을 준 사람
- 우아한테크코스 4기 스컬: https://iskull-dev.tistory.com/
'JAVA > JAVA | Spring 학습기록' 카테고리의 다른 글
[230327] javax.validation:validation-api에서 @Valid가 먹히지 않을 때 (feat. spring-boot-starter-validation) (0) | 2023.03.27 |
---|---|
[Java] PermGen 영역 대신 Metaspace가 도입된 Java 8 이후의 JVM 구조 및 JVM 튜닝 맛보기 (2) | 2023.01.27 |
[221231] 유스콘 2022 웨비나 컨퍼런스 후기 (8) | 2022.12.31 |
[Spring] 여러 포트에서 동일 애플리케이션 실행하기(Gradle, Maven) (0) | 2022.12.22 |
[Spring] MapStruct를 이용한 Entity <-> DTO 고찰 (2) (7) | 2022.12.10 |