5
votes

With csrf disabled I can upload file however I need it enabled. The problem only occurs when the form enctype is multipart/form-data, namely 'Invalid CSRF Token' with 403.

Generally when I set the enctype as multipart/form-data even for a form without file upload, I get the same error.

Using this dependency:

<dependency>
  <groupId>org.synchronoss.cloud</groupId>
  <artifactId>nio-multipart-parser</artifactId>
  <version>...</version>
</dependency>

Tried including hidden csrf input in the form and also tried appending it to the url but same error

    <form  method="post" th:action="${'/add/' + id + '/documents?' + _csrf.headerName + '=' + _csrf.token}"  enctype="multipart/form-data">
        <input type="file" name="documents" multiple="multiple">
        <input  type="hidden"
                th:name="${_csrf.headerName}"
                th:value="${_csrf.token}" />
        <input type="hidden" name="_csrf" th:value="${_csrf.token}">
        <button class="btn btn-success btn-l">Upload</button>
    </form>

Have a controller advice like this for csrf injection

@ControllerAdvice
public class SecurityAdvice {@ModelAttribute("_csrf")Mono<CsrfToken> csrfToken(final ServerWebExchange exchange) {
    final Mono<CsrfToken> csrfToken = exchange.getAttributeOrDefault(org.springframework.security.web.server.csrf.CsrfToken.class.getName(), Mono.empty());
    return csrfToken;
}

In security I have the following bean:

 @Bean
    public ServerCsrfTokenRepository csrfTokenRepository() {
        WebSessionServerCsrfTokenRepository repository =
                new WebSessionServerCsrfTokenRepository();
        repository.setHeaderName("X-CSRF-TK");

        return repository;
    }

and using it like this in my SecurityWebFilterChain:

.and().csrf().csrfTokenRepository(csrfTokenRepository())

UPDATE:

Disabling csrf for a few urls would be enough too. Found a few examples but all of them are for Servlet based version. https://sdqali.in/blog/2016/07/20/csrf-protection-with-spring-security-and-angular-js/

2

2 Answers

1
votes

Take a look at Spring Security's official recommendation: https://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html#csrf-multipart

There are basically two ways of doing it: (1) placing MultipartFilter before Spring Security filter and (2) include the CSRF token in the form action, as you are doing. The first option is the recommended one:

The first option is to ensure that the MultipartFilter is specified before the Spring Security filter. Specifying the MultipartFilter before the Spring Security filter means that there is no authorization for invoking the MultipartFilter which means anyone can place temporary files on your server. However, only authorized users will be able to submit a File that is processed by your application. In general, this is the recommended approach because the temporary file upload should have a negligble impact on most servers.

To ensure MultipartFilter is specified before the Spring Security filter with java configuration, users can override beforeSpringSecurityFilterChain as shown below:

public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

    @Override
    protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
        insertFilters(servletContext, new MultipartFilter());
    }
}

To ensure MultipartFilter is specified before the Spring Security filter with XML configuration, users can ensure the element of the MultipartFilter is placed before the springSecurityFilterChain within the web.xml as shown below:

<filter>
    <filter-name>MultipartFilter</filter-name>
    <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>MultipartFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Note that, if you still want to use form action, query parameters can be leaked. Try to change your "headerName" to "parameterName":

<form action="./upload?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">

EDIT: In case you can't switch to a servlet-based container (such as Jetty or Tomcat) and the form action recommendation doesn't work, there's a recent Stack Overflow thread discussing this issue.

One of the developers reported to workaround the issue using AJAX:

I solved this problem by:

  • sending the multi-part file using vanilla javascript, like in Mozilla's guide
  • adding the _csrf token in the HTML header, in meta tags, like in the Spring guideline for sending the CSRF token with Ajax
  • instead of using jquery, adding it directly to the XHR object

var csrfToken = $("meta[name='_csrf']").attr("content"); var csrfHeader = $("meta[name='_csrf_header']").attr("content"); XHR.setRequestHeader(csrfHeader, csrfToken); XHR.setRequestHeader('Content-Type','multipart/form-data; boundary=' + boundary); XHR.send(data);

The same developer reported this issue to Spring, but didn't get any attention yet.

0
votes

You can simply enable it in the filter chain.

Reference from documentation:

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .csrf(csrf -> csrf.tokenFromMultipartDataEnabled(true))
    return http.build();
}