CSRF : Cross-stie request forgery
사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위를 특정 웹사이트에 요청하게 하는 공격
요청이 리소스를 변경해야 하는 요청인지 확인하고, 맞으면 CSRF 토큰 검증
(기본 활성화)
public final class CsrfFilter extends OncePerRequestFilter {
// ✨ 맨 밑에 정의되어 있음
public static final RequestMatcher DEFAULT_CSRF_MATCHER = new DefaultRequiresCsrfMatcher();
// ✨ HttpSessionCsrfTokenRepository 구현체 클래스가 사용됩니다.
private final CsrfTokenRepository tokenRepository;
private RequestMatcher requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
// ✨ Csrf 발행된 토큰이 있는지 확인하고 가져옴!
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = (csrfToken == null);
// ✨ Csrf 토큰이 없으면 생성!
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);
return;
}
// ✨ 리소스가 변경될 수 있는 요청인 경우에는 여기서 검증을 하게 됩니다!
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
// ✨ 일치하지 않는다면 AccessDeniedException 예외 처리하는 것을 알 수 있습니다!
if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
this.logger.debug(
LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
: new MissingCsrfTokenException(actualToken);
this.accessDeniedHandler.handle(request, response, exception);
return;
}
filterChain.doFilter(request, response);
}
/**
* Constant time comparison to prevent against timing attacks.
* @param expected
* @param actual
* @return
*/
private static boolean equalsConstantTime(String expected, String actual) {
if (expected == actual) {
return true;
}
if (expected == null || actual == null) {
return false;
}
// Encode after ensure that the string is not null
byte[] expectedBytes = Utf8.encode(expected);
byte[] actualBytes = Utf8.encode(actual);
return MessageDigest.isEqual(expectedBytes, actualBytes);
}
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
// ✨ 읽기 전용인 요청에 대해서는 PASS 시킨다.
// → 리소스를 변경시키는 요청의 경우는 검증
private final HashSet<String> allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
@Override
public boolean matches(HttpServletRequest request) {
return !this.allowedMethods.contains(request.getMethod());
}
@Override
public String toString() {
return "CsrfNotRequired " + this.allowedMethods;
}
}
}
위에서 살펴보면 `hidden` 속성으로 csrf 값이 포함된것을 볼 수 있습니다.
저 value 값을 변경해서 로그인을 하게 된다면 `access denied`가 발생합니다!
이렇게 마음대로 변경해서 요청을 하게 되면?
위와 같이 Access Denied가 발생하는 것을 볼 수 있습니다.