Summary

We have a API that is used by both a same-origin Angular Single-Page Application and native apps.

The Angular SPA is not fully REST and uses cookies, so CSRF protection is required. It's enabled with http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); and it works automatically with Angular.

On the other hand, our native apps are REST and don't store or send cookies, so CSRF protection is not required. But we are forced to handle cookies and send the X-XSRF-TOKEN header.

It would be useful to have an option (disabled by default) that disables CSRF protection on requests that don't have any cookies and don't have HTTP Authentication (Authorization header). The rationale is that if a request does not have any user credentials (cookies or HTTP Auth), there is nothing to forge.

On such requests, CsrfFilter should also remove any Set-Cookie response headers to prevent CSRF login. The rationale is that a real user would request other resources with GET before sending the form, so we shouldn't initialize a session on a POST request.

Version

4.2.2.RELEASE

Comment From: rwinch

Thanks for the report!

At the moment, I'm not sure if we want to support this out of the box, because I don't know if it is that easy.

If you want to do this, you can leverage requiresCsrfProtectionMatcher to configure the app how you want. For example:

protected void configure(HttpSecurity http) throws Exception {
  http
    .csrf()
      .requireCsrfProtectionMatcher( r -> CsrfFilter.DEFAULT_CSRF_MATCHER.matches(r) && (r.getCookies() != null || r.getHeader("Authorization") != null))
      .and()
    ...
}

Or if you want to use built in types:

protected void configure(HttpSecurity http) throws Exception {
  RequestHeaderRequestMatcher basicAuth = new RequestHeaderRequestMatcher("Authorization");
  RequestHeaderRequestMatcher hasCookie = new RequestHeaderRequestMatcher("Cookie");
  RequestMatcher stateful = new NegatedRequestMatcher(new OrRequestMatcher(basicAuth, hasCookie));
  RequestMatcher csrfEnabled = new AndRequestMatcher(CsrfFilter.DEFAULT_CSRF_MATCHER, statefull);
  http
    .csrf()
      .requireCsrfProtectionMatcher( csrfEnabled )
      .and()
    ...
}

Comment From: imgx64

Thanks, that code works.

I still need to implement the second part about preventing CSRF login. I'll just write a filter for it.

Comment From: 2is10

A simpler way to bypass CSRF protection for all cookie-less requests is:

 .csrf()
     .requireCsrfProtectionMatcher(new AndRequestMatcher(
         CsrfFilter.DEFAULT_CSRF_MATCHER,
         new RequestHeaderRequestMatcher(HttpHeaders.COOKIE)))

Checking for the Cookie header ought to be baked into the default matcher, if you ask me. The cookie mechanism is what creates the CSRF vulnerability; it’s the attack vector.

Comment From: rwinch

@2is10 Credentials can be automatically included in other ways too. For example, if basic authentication is used or supported the credentials are also automatically included in the request.

Comment From: 2is10

@rwinch A browser will automatically include basic authentication credentials to domain X in requests initiated by domain Y? Where can I learn more about this? Thanks.

Comment From: rwinch

I'm closing this as won't fix. As mentioned in the discussion there are other ways of including credentials that might happen automatically (i.e. HTTP Basic is automatically included in a request).

Comment From: jzheaux

@2is10 This article from PortSwigger talks about HTTP Basic as another vector for CSRF and is generally a nice introduction to CSRF.