1
0
mirror of synced 2026-05-22 21:33:16 +00:00

Add CsrfFilter.csrfRequestAttributeName

Previously the CsrfToken was set on the request attribute with the name
equal to CsrfToken.getParameterName(). This didn't really make a lot of
sense because the CsrfToken.getParameterName() is intended to be used as
the HTTP parameter that the CSRF token was provided. What's more is it
meant that the CsrfToken needed to be read for every request to place it
as an HttpServletRequestAttribute. This causes unnecessary HttpSession
access which can decrease performance for applications.

This commit allows setting CsrfFilter.csrfReqeustAttributeName to
remove the dual purposing of CsrfToken.parameterName and to allow deferal
of reading the CsrfToken to prevent unnecessary HttpSession access.

Issue gh-11699
This commit is contained in:
Rob Winch
2022-08-11 15:50:37 -05:00
parent 666f175225
commit 5b64526ba9
9 changed files with 110 additions and 1 deletions
@@ -87,6 +87,8 @@ public final class CsrfFilter extends OncePerRequestFilter {
private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
private String csrfRequestAttributeName;
public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
this.tokenRepository = csrfTokenRepository;
@@ -108,7 +110,9 @@ public final class CsrfFilter extends OncePerRequestFilter {
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
String csrfAttrName = (this.csrfRequestAttributeName != null) ? this.csrfRequestAttributeName
: csrfToken.getParameterName();
request.setAttribute(csrfAttrName, csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Did not protect against CSRF since request did not match "
@@ -167,6 +171,18 @@ public final class CsrfFilter extends OncePerRequestFilter {
this.accessDeniedHandler = accessDeniedHandler;
}
/**
* The {@link CsrfToken} is available as a request attribute named
* {@code CsrfToken.class.getName()}. By default, an additional request attribute that
* is the same as {@link CsrfToken#getParameterName()} is set. This attribute allows
* overriding the additional attribute.
* @param csrfRequestAttributeName the name of an additional request attribute with
* the value of the CsrfToken. Default is {@link CsrfToken#getParameterName()}
*/
public void setCsrfRequestAttributeName(String csrfRequestAttributeName) {
this.csrfRequestAttributeName = csrfRequestAttributeName;
}
/**
* Constant time comparison to prevent against timing attacks.
* @param expected
@@ -48,6 +48,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
@@ -344,6 +345,23 @@ public class CsrfFilterTests {
assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setAccessDeniedHandler(null));
}
// This ensures that the HttpSession on get requests unless the CsrfToken is used
@Test
public void doFilterWhenCsrfRequestAttributeNameThenNoCsrfTokenMethodInvokedOnGet()
throws ServletException, IOException {
CsrfFilter filter = createCsrfFilter(this.tokenRepository);
String csrfAttrName = "_csrf";
filter.setCsrfRequestAttributeName(csrfAttrName);
CsrfToken expectedCsrfToken = mock(CsrfToken.class);
given(this.tokenRepository.loadToken(this.request)).willReturn(expectedCsrfToken);
filter.doFilter(this.request, this.response, this.filterChain);
verifyNoInteractions(expectedCsrfToken);
CsrfToken tokenFromRequest = (CsrfToken) this.request.getAttribute(csrfAttrName);
assertThat(tokenFromRequest).isEqualTo(expectedCsrfToken);
}
private static CsrfTokenAssert assertToken(Object token) {
return new CsrfTokenAssert((CsrfToken) token);
}