1. CSRF(Cross-site Request Forgery)
공부해봤던 공격 방식이다. 쇼핑몰 사이트에 사용자가 로그인을 해서 토큰을 가진채로 공격자가 보낸 링크를 클릭하는 경우, 공격자가 해당 쇼핑몰 사이트에서 배송지를 변경하는 javascript 코드를 심어놓았다면 서버는 사용자의 토큰과 이 코드를 신뢰하여 공격자의 의도대로 배송지를 변경해버리게 된다. 즉, 사용자의 의도와는 무관하게 서버에 요청을 하는 공격 방식을 말한다.
이를 방지하기 위해서 서버는 사용자의 요청 시마다 난수로 생성되는 CSRF Token을 발행한다. 이 토큰은 페이지에 hidden 객체로 들어가게 된다. 그리고 요청시마다 서버에 보내어 토큰의 일치 여부를 서버에서 판단하기 때문에, 공격자가 사용자의 세션 정보를 갖고 있다하더라도 요청마다 발행되는 CSRF Token 정보는 없기 때문에 CSRF를 막을 수 있다.
물론 사용자가 어떤 요청 단계에서 갖고 있는 CSRF Token까지 탈취하여 새로운 요청을 하기 전에 서버에 요청을 보낸다면 공격자의 의도대로 요청을 보낼 수 있다. 그러나 이런 방법은 난이도가 어려워서 적어도 단순히 사용자에게 어떤 페이지를 보여주는 방식은 통하지 않게 된다.
2. CSRF Filter
기본적으로 스프링 시큐리티에서 CSRF Filter를 적용해놓는다. http.csrf() 구문으로 동작하며, 만약 csrf filter의 동작을 해제하고 싶다면 http.csrf().disabled()를 실행시켜주면 된다.
동작 방식 확인을 위해서 CSRFFilter에 디버깅을 해본다.
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Did not protect against CSRF since request did not match " + this.requireCsrfProtectionMatcher);
}
filterChain.doFilter(request, response);
} else {
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
this.logger.debug(LogMessage.of(() -> {
return "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request);
}));
AccessDeniedException exception = !missingToken ? new InvalidCsrfTokenException(csrfToken, actualToken) : new MissingCsrfTokenException(actualToken);
this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception);
} else {
filterChain.doFilter(request, response);
}
}
}
doFilterInternal 메서드의 첫 번째 줄에 break point를 걸고 아무 요청이나(루트 페이지 접속) 보내보면, 아래와 같이 csrfToken이 생성되는 것을 확인할 수 있다. 여기서 delegate 객체를 살펴보면 된다.
- token은 난수로 생성되는 CSRFToken의 value값이다.
- parameterName은 클라이언트의 페이지에 hidden으로 전달되는 CSRFToken의 이름값이다.
- headerName은 CSRFToken의 name(key)값이다.
확인을 위해서 여러 번 요청도 보내보고, F9 키도 눌러보면, 그때마다 token값이 달라지는 것을 볼 수 있다.
그리고 클라이언트상에서도 개발자도구에서 csrfToken을 <input type="hidden"> 태그 형태로 확인할 수 있다.
추가로 강의에서는 크롬 확장 프로그램 Talend API Tester을 이용해서 CSRFToken을 강제로 제거한 뒤 요청을 보내보고, 서버의 token값을 요청의 header에 삽입하여 요청을 보내서 테스트도 해보았다. 어쨌든 중요한 것은 요청마다 csrf값을 넣어주고 이를 CSRF Filter에서 처리한다는 점일 것이다.
참조
1) 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 정수원님 강의
2) 절차대로 생각하고 객체로 코딩하기 블로그- Spring Security_CSRF Token의 개념과 사용 방법
https://codevang.tistory.com/282
'Programming-[Backend] > Spring Security' 카테고리의 다른 글
[스프링 시큐리티] 10. 프로젝트 생성, PostgreSQL, PasswordEncoder, WebIgnore (0) | 2022.01.13 |
---|---|
[스프링 시큐리티][작성중][메모] 9. 주요 아키텍처 이해 (0) | 2022.01.09 |
[스프링 시큐리티] 7. 예외처리 : ExceptionTranslationFilter, 요청 캐시 필터 : RequestCacheAwareFilter (0) | 2022.01.09 |
[스프링 시큐리티] [작성중] 6. 권한 설정, 표현식 (0) | 2022.01.08 |
[스프링 시큐리티] [작성중] 5. 익명 사용자 인증 필터, 세션 제어 필터 (0) | 2022.01.07 |