1. 개요
- CSRF의 정의
- Spring Boot에서의 CSRF Filter 처리 방식
2. CSRF란?
사이트 간 요청 위조(Cross-site request forgery, CSRF)는 웹사이트 취약점 공격의 하나로, 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 하는 공격을 말한다. - 위키백과
- 특정사이트에 로그인 된 상태에서 메일이나 게신판들을 열람했을 때 자신의 의지와는 상관없이 수정,삭제,등록 등이 일어나는 행위를 말한다.(같은 브라우저상에서는 탭이 다르더라도 세션이 (JSESSIONID등의)쿠키로 공유 되기 때문이다.)
- 밑에 설명은 같은 사이트내에서 발생한 예제이지만, 이름에서 알수 있듯이 보통은 타 사이트에서에 요청을 차단하는 것이다.
안녕하세요. 저는 아무개 커뮤니티의 회원 "talmobil"이라고 합니다.
유머게시판에 게시글 하나를 썼는데 좋아요가 너무 없어서 자괴감이 들었습니다. 아니, 열받더라구요. 제 글이 정말 재밌는데, 사람들이 일부러 좋아요를 눌러주지 않는 것 같거든요. -ㅅ-
그러던 중 좋은 방법이 하나 떠올랐습니다. 다른 사람이 제 게시글을 보면 좋아요를 자동으로 누르게 하는거죠!
방법은 이래요.
커뮤니티 사이트에서 게시글에 좋아요를 누르니 "http://community.com/like?post=[게시글 id]"이더군요.
제 게시글 ID를 확인해보니 9566 이구요. 그래서 게시글 안에 이미지 태그를 하나 삽입하고 src 값에 "http://community.com/like?post=9566"을 넣어봤습니다. 그랬더니 사람들이 제 게시글을 조회하게 되면 이미지 태그의 URL인 "http://community.com/like?post=9566" 가 자동으로 호출되었습니다! @_@.. 자동으로 제 게시글의 좋아요 수가 올라갔어요. 후후.. 다른 커뮤니티 사이트에도 어그로 게시글을 올리고 마찬가지로 src 태그를 넣었더니 더 빨리 오르더군요!
사용자들은 아무것도 모르고 제가 의도한대로 요청을 하고있는겁니다! 이제는 좋아요가 아닌 다른걸 해봐야겠어요 흐ㅏㅎ하
- 아무것도 모르는 사용자들은 공격자가 의도한 행위를 사이트간 위조 요청으로 하게 되었다. 이런 공격이 바로 CSRF 이다. 그렇다면 이러한 공격을 막는 방법이 있을까? 당연히 있다. 서버에서 요청에 대한 검증을 하는 것이다. 이를테면 토큰 값으로 말이다. 이것도 예를 들어보겠다.
안녕하세요. 아무개 커뮤니티 담당자 "ㅅㄱ"입니다.
제가 집에 우환이 생겼습니다. 웃음이 필요해서 유머 게시판을 쓱 둘러봤는데, 개노잼 글이 하나 있더군요. 뭐지 하고 넘어가려는데 이 게시글의 좋아요 수가 무려 1만이 넘어갔습니다. 그리고 다시 들어가보니 누르지도 않은 좋아요가 눌러져있더라구요. 이게 말이되나? 싶었습니다.
서버 로그를 확인해보니 제가 좋아요를 누른 기록도 있었습니다. 뭔가 이상해서 게시글 내용을 살펴봤는데 이미지 태그의 src에 좋아요 처리를 하는 URL이 들어가 있었습니다. 해당 URL을 구글링 해보니 이미 다른 웹사이트 게시글에도 포함되어 있더군요. 말로만 듣던 CSRF 공격이었습니다.
막을 방법은 클라이언트마다 토큰을 발급하는 겁니다. 서버는 토큰 값을 검증하고요. 프로세스는 다음과 같습니다.
1. 저희 커뮤니티를 접근하면 특정 토큰을 클라이언트에게 발급함과 동시에 저희 서버 세션 안에 넣습니다.
> A 클라이언트에 대해 A 토큰을, B클라이언트에 대해 B 토큰을 이렇게 각각 발급하는 겁니다.
2. 클라이언트는 모든 API를 호출할 때 필수적으로 이 토큰 값을 헤더에 넣어 보냅니다.
3. 서버에서는 요청을 수행하기전 Filter 레벨에서 세션 안에 들어있는 토큰 값과 요청 토큰 값을 비교합니다.
4. 토큰 값이 불일치할 경우 비정상적인 요청으로 판단하고 Access Denied 시킵니다.
토큰 검증을 성공하려면 요청 시 CSRF Token 값을 헤더에 넣어줘야하는데, 공격자는 사용자마다 각각 발급된 토큰 값을 알 수 없기때문에 막힐겁니다.
추가적으로, 이러한 방식을 스프링 시큐리티에서 기본적으로 지원하고 있더라구요!
- 이처럼 서버에서 토큰을 발급 및 검증하고 클라이언트에서는 발급받은 토큰을 요청 값에 포함시켜 보내는 방식으로 CSRF 공격을 막을 수 있다. 스프링 시큐리티 의존성을 추가하면 이와 같은 방식을 제공하는 CSRF Filter가 자동으로 추가된다. csrf().disable() 설정을 통해 해제도 가능하다. 그럼 Spring Security에서 제공하는 CSRF Filter는 요청을 어떻게 처리하는지 알아보자.
3. CSRF Filter
CsrfFilter.java
- 위 소스는 Spring Security에서 제공하는 CSRF Filter의 내부 소스이다.
- 빨간색 표시된 부분이 중요한 부분이다. 하나하나 설명해보겠다.
3.1. tokenRepository.loadToken(request)
- 요청 세션에서 CSRF Token 객체를 조회한다. key는 HttpSessionCsrfTokenRepository.CSRF_TOKEN 이다.
tokenRepository 구현체
3.2. tokenRepository.generateToken(request)
- CSRF Token이 없을 경우 DefaultCsrfToken 생성자를 통해 CSRF Token을 발급한다.
- 생성자 마지막 파라미터로 createNewToken() 리턴 값을 넣고있는데, token 값으로 사용할 랜덤 값을 생성한다.
createNewToken 메서드
DefaultCsrfToken 생성자
- 생성자 메서드 호출 후 CsrfToken 객체를 생성하면 다음과 같은 형태의 CSRF Token 객체가 생성된다.
CsrfToken | |
token | Random UUID |
parameterName | _csrf |
headerName | X-CSRF-TOKEN |
3.3. tokenRepository.saveToken()
- 세션 내에 key = HttpSessionCsrfTokenRepository.CSRF_TOKEN, value = 생성한 CsrfToken 객체를 생성한다.
3.4. request.setAttribute(CsrfToken.class.getName(), csrfToken)
- HttpServletRequest 를 통해 csrfToken 값을 조회할 수 있도록 설정해주는 부분이다.
3.5. requireCsrfProtectionMatcher.matches(request)
- Csrf 검증 메서드를 체크한다. 기본적으로 GET, HEAD, TRACE, OPTIONS를 제외한 모든 메서드에 대해서는 CsrfToken을 검증한다. 만약 GET으로 요청이 들어왔다면 검증 없이 다음 Filter로 넘어간다.
3.6. request.getHeader(csrfToken.getHeaderName()) , getParameter(csrfToken.getParameterName());
- 요청 헤더에 X-CSRF-TOKEN 값이 있는지 확인하고, 없을 경우 요청 바디에서 _csrf 값이 있는지 확인한다.
- 클라이언트는 요청 헤더의 X-CSRF-TOKEN 혹은 요청 바디의 _csrf 값 둘 중 하나로 CsrfToken 값을 보내면 된다.
3.7. equalsConstantTime(csrfToken.getToken(), actualToken)
- 세션에 저장(혹은 생성)한 토큰값과 클라이언트에서 보낸 토큰 값을 비교하여 일치할 경우 다음 필터를 호출하고 불일치할 경우 accessDeniedHandler 메서드를 호출하여 예외 처리한다.
4. 테스트
4.1. Spring Security CSRF 설정
- 크게 xxx_security.xml의 xml에 csrf disabled="true" 설정하는 방법과 밑에와 같이 config클래스를 상속받아 설정하는 방법있다.
- Spring Security에서는 @EnableWebSecurity 어노테이션을 지정할 경우 자동으로 CSRF 보호 기능이 활성화가 됩니다.
- 따라서 CSRF 비활성화가 필요할 경우 아래와 같이 csrf().disable() 설정을 추가합니다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
}
4.2. 화면단에 CSRF 토큰 추가
- form 태그 안에 아래와 같은 코드를 추가하거나, 스프링 시큐리티 taglib 추가 후 <s:csrfInput/> 태그를 추가하면 발급받은 _csrf 토큰을 자동으로 set 해준다.
- API 호출 시 정상적으로 처리되는 것을 확인할 수 있다.
csrf 구문 추가
자동 추가된 csrf 값
정상 처리된 post 메서드
헤더,쿠키,파라메터로 세팅가능하며 별도로 따로 세팅하지 않으면 주고받는 변수명은 밑에와 같다.
static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
5. 회고
- 스프링 시큐리티를 설정 시 갑자기 발생한 403 에러에 당황했던 적이 많다. 찾아보면 csrf().disable() 한줄을 추가 하면 됐고, 간단한 설정처럼 보였던 이 CSRF가 뭔지는 크게 궁금하지 않았다. 개념정도만 짚고 넘어갔다. 그런데 나중에 똑같은 에러에 똑같이 당황하게 됐고, 똑같이 구글링하고 개념만 쓱 보게 되더라. 그때도 머릿속에 개념이 잘 잡히지 않았는데, 프로세스 정리를 쭉 하니 확실하게 이해하게 된 것 같다.
6. 나의 경험
- get은 기본적으로 검사하지 않는다.
이유를 검색해보면 악의적인 수정 삭제 등록이 POST때 이루지기 때문이라고 돼있던데요? 정확한 이유는 모르겠다.
(OKKY - spring security csrf에서 get방식은 검사하지 않는 이유가 뭔가요??)
- 다른사이트에 내용을 검색하다보면 마치 웹사이트에서 접근할때마다 CSRF 토큰이 계속 새로 발급되는 것 처럼
나와있는데, 아니다.
세션당 하나 발급된다. 최초 접근시 세션에서 토큰이 없으면 토큰 발급 후 세션에 저장 후 화면단에
파라미터나 헤더로 리턴해준다.
화면에서는 그걸 받아 요청시 받은 토큰을 넘겨준다.
세션에 저장 된 토큰과 화면단에서 던지는 토큰을 비교한다.
- 나의 경우 이유는 알수 없지만 response header에 토큰 값이 넘어 오지 않았다.
게다가 특정 기능을 추가하고 싶어서 따로 수동으로 Spring security의 crsfFilter 를 개발해서
filter등록 후 별도로 사용했다. 파일은 밑에 첨부했다.
- 위와 같이 수동으로 필터 구성시에는 세션 체크 필터보다 앞에서 실행 되면 세션이 끊어진 상태에서
요청 됨으로 토큰 체크오류가 났다.
세션체크 필터보다 뒤에서 실행 돼야한다.
- 세션 타임아웃등으로 세션이 비정상적으로 종료 됐을경우,
자동으로 이동되는 페이지에서 session.invalidate();로 완전히 세션을 무효화 해야한다.
그렇지 않은경우 이상하게 기존 토큰값을 물고 오는 경우가 생겼다.
- 쿠키를 사용중이고 공통단에서 헤드값에 토큰을 넣고 있다면 가끔 rquestHeader에
예전 토큰 값이 넘어오는 경우가 생겼다.
쿠키가 생기지 않도록 설정이 필요했음.(사이트 속도에 영향을 줄수있음)
=> 토큰 자체를 헤더말고 파라메터에 넣어도 되지만 그러면 이미 개발 된 화면단에 전체를 고처야하는 부분이 생겨
이 방법으로 선택
(이당시 넥사크로14로 개발했고 넥사크인경우 헤더를 등록하면 계속 유지 되기 때문에 가능
, JSP라면 공통으로 만들어서 include해서 사용한다면 기존에 만들어진 소스에 내용 수정을
최소화 할수는 있을듯하다.)
출처 : https://tlatmsrud.tistory.com/77
'웹개발 > spring' 카테고리의 다른 글
Spring Java 수정 시 톰캣 재 실행 안하고 적용하기. (0) | 2022.07.26 |
---|---|
Spring Boot 프로젝트 생성 (0) | 2022.07.01 |
스프링 properties 읽어오기 (0) | 2022.02.17 |
스프링 Filter 와 Listener (0) | 2022.02.16 |
스프링 Bean의 개념과 Bean Scope 종류 (0) | 2022.02.16 |
댓글