25

We have been using Spring Security with our application for a few years now. Last week we upgraded Spring Security from version 3.1.4 to 3.2.0. The upgrade went fine and we did not find any errors post the upgrade.

While looking through the Spring Security 3.2.0 documentation we came across the newly added features around CSRF protection and security headers. We followed the instructions in the Spring Security 3.2.0 documentation to enable CSRF protection for our protected resources. It works fine for regular forms but does not work for multipart forms in our application. On form submission, CsrfFilter throws an Access Denied error citing the absence of a CSRF token in the request (determined through DEBUG logs). We have tried using the first option suggested in the Spring Security documentation for making CSRF protection work with multipart forms. We do not want to use the second suggested option as it leaks CSRF tokens through the URLs and poses a security risk.

The relevant part of our configuration based on the documentation is available as a Gist on Github. We are using Spring version 4.0.0.

Note that we have already tried the following variations without success:

  1. Not declaring the MultipartFilter in web.xml.
  2. Not setting the resolver bean name for the MultipartFilter in web.xml.
  3. Using the default resolver bean name filterMultipartResolver in webContext.xml.

UPDATE: I have confirmed that the documented behaviour does not work even with a single page sample app. Can anyone confirm that the documented behaviour works as expected? Is there an example working application that can be used?

manish
  • 19,695
  • 5
  • 67
  • 91

4 Answers4

49

I was able to resolve this with help from the Spring Security team. I have updated the Gist to reflect a working configuration. I had to follow the steps given below in order to get everything to work as expected.


1. Common Step

Add a MultipartFilter to web.xml as described in the answer by @holmis83, ensuring that it is added before the Spring Security configuration:

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

<filter>
    <display-name>springSecurityFilterChain</display-name>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>ERROR</dispatcher>
    <dispatcher>FORWARD</dispatcher>
    <dispatcher>REQUEST</dispatcher>
</filter-mapping>

2.1. Using Apache Commons Multipart Resolver

Ensure that there is an Apache Commons Multipart Resolver bean named filterMultipartResolver in the root Spring application context. I will stress this again, make sure that the Multipart Resolver is declared in the root Spring Context (usually called applicationContext.xml). For example,

web.xml

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
        classpath*:springWebMultipartContext.xml
    </param-value>
</context-param>

springWebMultipartContext.xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="filterMultipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize" value="100000000" />
    </bean>
</beans>

Make sure that the bean is called filterMultipartResolver as any other bean name is not picked up by MultipartFilter configured in web.xml. My initial configuration was not working because this bean was named multipartResolver. I even tried passing the bean name to MultipartFilter using web.xml init-param but that did not work either.

2.2. Using Tomcat Multipart support

Tomcat 7.0+ has in-built multipart support, but it has to be explicitly enabled. Either change the global Tomcat context.xml file as follows or include a local context.xml file in your WAR file for this support to work without making any other changes to your application.

<Context allowCasualMultipartParsing="true">
    ...
</Context>

After these changes using Apache Commons Multipart Resolver our application is working so far on Tomcat, Jetty and Weblogic.

Community
  • 1
  • 1
manish
  • 19,695
  • 5
  • 67
  • 91
  • 6
    Upvoting for "Make sure that the bean is called **filterMultipartResolver** as any other bean name is not picked up by MultipartFilter configured in web.xml". It also applies to annotation configuration, bean method name should be filterMultipartResolver. Example: `@Bean public MultipartResolver filterMultipartResolver() {...}` Before doing this, I was getting the error: ** java.lang.IllegalStateException: Unable to process parts as no multi-part configuration has been provided**. – Utku Özdemir May 16 '14 at 19:23
  • Thanks, the docs didn't help me get the issue resolved. I had to add Apache Commons File Upload to my pom.xml to get everything working. – arcdegree Jul 08 '14 at 18:01
  • Thanks - these instructions were very precise and clear. However - I followed them to the last letter but still continued to get the csrf mismatch error (found via debug). I was finally forced to add a hidden field with the _csrf component in order to get it to work. Using Spring Security 3.2.4, Spring 4.0.x and all these pages have requires-channel https on them. Any idea what I could have done wrong? – dipan66 Jul 31 '14 at 02:10
  • Oh yes, you have to add the CSRF token in forms as a hidden field for sure. I did not include that in the instructions above because that part is already described fully in the Spring Security docs for the most commonly used view technologies. – manish Jul 31 '14 at 03:56
  • This didn't work for me. I am getting `XSRF token mismatch (null). Session may be expired.` and I am uploading files using ajax. Thank you – JavaTechnical Oct 07 '14 at 17:13
  • Have you checked if the CSRF token is included in the AJAX request? You can use the browser's debugging tool to check the request being submitted to the server or use server-side debugging to examine the incoming request. Most likely the CSRF token is not included in the request and hence the error. Check the Spring Security documentation for details on including the token with AJAX requests. – manish Oct 09 '14 at 06:43
  • 1
    Everytime I set this filters I get always an empty uploaded file, any idea why? – jpganz18 Jun 30 '15 at 21:50
  • 1
    I would like to make more evident that the multipart resolver bean named `filterMultipartResolver` must stay in the **ROOT** Spring application context as opposed to the one that is used in a (non-CSRF) setup without filter, that must be named `multipartResolver` and can stay in the **WEB** Spring application context – Testo Testini Oct 09 '15 at 23:31
  • did everything as mentioned @manish but it isn't working for me. Though I'm getting the _csrf as a definitive parameter in the multipart request rather than inside the multipart data, but I'm still getting ACCESS DENIED. And Ya I'm not processing a AJAX request, mine is a simple form based post request – Lalit Mehra Oct 23 '15 at 10:35
  • As per the debugging, the request still isn't having the _csrf parameter which is causing the ACCESS DENIED behavior. Also I'm not receiving any of the parts of the multipart request in the filter. – Lalit Mehra Oct 23 '15 at 11:07
  • 1
    I second the thought of @jpganz18 as I'm too getting empty uploaded file and my multipart filter cannot find the parts in the request. Any thoughts on this ?? – Lalit Mehra Oct 23 '15 at 13:10
  • @jpganz18 I resolved it by changing the location of bean MultipartFilter to applicationContext.xml – Lalit Mehra Oct 23 '15 at 14:22
  • As of Spring 3.2.18 this answer is deprecated and results is empty files passed to controller. However this was correct configuration in earlier versions of Sping MVC. Configuration based on servlets 3 described in Spring documentation is now error free and works as intended. – Talijanac Oct 09 '17 at 14:27
  • This worked perfectly for me. I wrote a custom CSRFFilter (because I'm on Spring Security 2) and added it to the springSecurityFitlerChain. Because of this, I needed to make sure springMultipartFilter was defined in web.xml before the springSecurityFilterChain so it would execute before the CSRFFilter. – splashout Nov 20 '17 at 21:45
5

This part:

<filter-mapping>
    <filter-name>multipartFilter</filter-name>
    <servlet-name>/*</servlet-name>
</filter-mapping>

Should be:

<filter-mapping>
    <filter-name>multipartFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

It is an error in Spring Security 3.2.0 documentation. The bug has been reported and will be fixed in upcoming version.

holmis83
  • 15,922
  • 5
  • 82
  • 83
  • Thanks @holmis83 for the suggestion. This was indeed one of the reasons why our set up was not working. However, there were other issues with the configuration as well that I was able to resolve with help from the Spring Security team. Please see my answer to this question for the full configuration that got our application working. – manish Jan 30 '14 at 04:54
4

After struggling with this issue a bit, I found a much easier solution by just using the Request Header defined in Spring Security instead of trying to get the CSRF token embedded as a part of the multipart content.

Here is a simple way I setup the header using an AJAX library for file upload in my jsp:

var uploader = new AjaxUpload({
        url: '/file/upload',
        name: 'uploadfile',
        multipart: true,
        customHeaders: { '${_csrf.headerName}': '${_csrf.token}' },
        ...
        onComplete: function(filename, response) {
            ...
        },
        onError: function( filename, type, status, response ) {
            ...
        }
});

Which in turn sent the multipart request with header:

X-CSRF-TOKEN: abcdef01-2345-6789-abcd-ef0123456789

Their recommendations for embedding into <meta /> tags in the header would also work just fine by halting the request on submit, adding the header via javascript, and then finish submitting:

<html>
<head>
    <meta name="_csrf" content="${_csrf.token}"/>
    <!-- default header name is X-CSRF-TOKEN -->
    <meta name="_csrf_header" content="${_csrf.headerName}"/>
    <!-- ... -->
</head>
<body>
    <!-- ... -->
    <script>
        var token = $("meta[name='_csrf']").attr("content");
        var header = $("meta[name='_csrf_header']").attr("content");
        // Do whatever with values
    </script>
</body>
</html>

More info: Spring Security - CSRF for AJAX and JSON Requests

rwyland
  • 1,637
  • 3
  • 16
  • 30
  • This also works with vanilla javascript (no additional library), like I described here http://stackoverflow.com/questions/21514074/spring-csrf-token-does-not-work-when-the-request-to-be-sent-is-a-multipart-requ/43122490#43122490 – Andrei Epure Mar 30 '17 at 15:56
2

Find most answer is answered server years ago.

If you need

Passing CSRF tokens with RestTemplate

This blog is quite enlightening https://cloudnative.tips/passing-csrf-tokens-with-resttemplate-736b336a6cf6

In Spring Security 5.0.7.RELEASE

https://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html#csrf-multipart

There are two options to using CSRF protection with multipart/form-data. Each option has its tradeoffs.

-Placing MultipartFilter before Spring Security
-Include CSRF token in action

For short, the first option is safer, the latter is easier.

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 negligible 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>

Another option

If allowing unauthorized users to upload temporary files is not acceptable, an alternative is to place the MultipartFilter after the Spring Security filter and include the CSRF as a query parameter in the action attribute of the form. An example with a jsp is shown below

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

The disadvantage to this approach is that query parameters can be leaked. More genearlly, it is considered best practice to place sensitive data within the body or headers to ensure it is not leaked.

Shihe Zhang
  • 2,641
  • 5
  • 36
  • 57
  • Initially I used to put the CSRF parameter as the input tag in the form tag, it was not working. then I move the CSRF parameter as query string in action attribute in the form tag, it works. thanks for detailing – ParagFlume Aug 06 '18 at 17:38
  • @ParagFlume glad to hear the answer is helpful. If not a bother, you could upvote to support it. – Shihe Zhang Aug 07 '18 at 01:05