Spring CSRF token does not work, when the request to be sent is a multipart request

22,592

Solution 1

In this case, since it is a multipart request in which the CSRF token is unavailable to Spring security unless MultipartFilter along with MultipartResolver is properly configured so that the multipart request can be processed by Spring.

MulipartResolver in the applicationContext.xml file has to be registered as follows

<bean id="filterMultipartResolver" 
      class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> 

    <property name="maxUploadSize" value="-1" />
</bean> 

The attribute value -1 of maxUploadSize puts no limit on the uploaded file size. This value may vary depending upon the requirements. In case of multiple files, the file size is the size of all uploaded files.


Also,

<servlet-name>/*</servlet-name> 

of <filter-mapping> of MultipartFilter needs to be changed to

<url-pattern>/*</url-pattern>

This is a bug in the documentation.

This will work just fine, in case, it is Spring MVC alone.

but if it is an integration of Spring and Struts(2), it incurs another problem in the associated Struts action class. The information of the uploaded file will be null in the associated Struts action class(es).

To solve this particular issue, see this answer to customize a multipart request.

Solution 2

If you are using @annotations, and the jsp view like this:

    <form:form id="profileForm" action="profile?id=${param.id}" method="POST" 
          modelAttribute="appUser" enctype="multipart/form-data" >
             ...
            <input type="file" name="file">
             ...
            <input type="hidden" name="${_csrf.parameterName}"
                value="${_csrf.token}" />
    </form:form>

this may help:

AppConfig.java :

@EnableWebMvc
@Configuration
@Import({ SecurityConfig.class })
public class AppConfig {

   @Bean(name = "filterMultipartResolver")
   public CommonsMultipartResolver filterMultipartResolver() {
      CommonsMultipartResolver filterMultipartResolver = new CommonsMultipartResolver();
      filterMultipartResolver.setDefaultEncoding("utf-8");
      // resolver.setMaxUploadSize(512000);
      return filterMultipartResolver;
}
...

The SecurityConfig.java extends WebSecurityConfigurerAdapter and is the configuration for SpringSecurity

The multipart/form-data filter (MultipartFilter) needs to be registered before the SecurityConfig that enables the CSRF. You can do it with this:

SecurityInitializer.java:

public class SecurityInitializer extends
AbstractSecurityWebApplicationInitializer {

@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
   super.beforeSpringSecurityFilterChain(servletContext);

   // CSRF for multipart form data filter:
   FilterRegistration.Dynamic springMultipartFilter;
   springMultipartFilter = servletContext.addFilter(
    "springMultipartFilter", new MultipartFilter());
   springMultipartFilter.addMappingForUrlPatterns(null, false, "/*");

}
}
Share:
22,592
Tiny
Author by

Tiny

Just an orphan kid and have no more to say. Three things in general, cannot be avoided (at least I can never) Mother Mother-tongue Mother-land. They are always unique. I'm a family-less boy. My family was hunted leaving me all alone when my house targeted and deliberately set on a fire by a mob during a nonsense communal riot but I was survived by a rescue team with the help of firemen. As a survival, I didn't know whether it was my fortune or misfortune but when I recovered, the rescue team came to my home, one day. One of the members gave me a piece of paper in my hand in which the following text was written. lifeisnowhere. He asked me to read it carefully and I could hardly interpret the text as Life is now here, instead of Life is nowhere. All of them gave me a cute smile and went away and I decided to live peacefully and hopefully on their saying from then onwards and very soon. Because of this tragedy, I'm alone couldn't join a school but a curiosity to learn something made me a self-learner. I'm indeed a self-learner, so I'm likely not able to answer any questions on this site right now. In the field of computer science, my self-study mainly includes, QBASIC, C, C++, C#, VB, Java, JavaScript, PHP and a little about ASP.NET. Oracle, MySQL and MSSQL-Server with DBMS. and other theoretical subjects. I'm currently dealing with - Android and Java EE including Servlet, JSP-JSTL/EL (with Spring and Struts with ORM models JPA/Hibernate) and JSF.

Updated on July 09, 2022

Comments

  • Tiny
    Tiny almost 2 years

    I use,

    • Spring Framework 4.0.0 RELEASE (GA)
    • Spring Security 3.2.0 RELEASE (GA)
    • Struts 2.3.16

    In which, I use an in-built security token to guard against CSRF attacks.

    The Struts form looks like the following.

    <s:form namespace="/admin_side"
            action="Category"
            enctype="multipart/form-data"
            method="POST"
            validate="true"
            id="dataForm"
            name="dataForm">
    
        <s:hidden name="%{#attr._csrf.parameterName}"
                  value="%{#attr._csrf.token}"/>
    </s:form>
    

    The generated HTML code is as follows.

    <form id="dataForm"
          name="dataForm"
          action="/TestStruts/admin_side/Category.action"
          method="POST"
          enctype="multipart/form-data">
    
        <input type="hidden"
               name="_csrf"
               value="3748c228-85c6-4c3f-accf-b17d1efba1c5" 
               id="dataForm__csrf">
    </form>
    

    This works fine, unless the request is multipart in which case, the request ends with the status code 403.

    HTTP Status 403 - Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'.

    type Status report

    message Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'.

    description Access to the specified resource has been forbidden.

    The spring-security.xml file is as follows.

    <?xml version="1.0" encoding="UTF-8"?>
    <beans:beans xmlns="http://www.springframework.org/schema/security"
                 xmlns:beans="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-4.0.xsd
               http://www.springframework.org/schema/security
               http://www.springframework.org/schema/security/spring-security-3.2.xsd">
    
        <http pattern="/Login.jsp*" security="none"></http>
    
        <http auto-config='true' use-expressions="true" disable-url-rewriting="true" authentication-manager-ref="authenticationManager">
            <session-management session-fixation-protection="newSession">
                <concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
            </session-management>
    
            <csrf/>
    
            <headers>
                <xss-protection />
                <frame-options />
                <!--<cache-control />-->
                <!--<hsts />-->
                <content-type-options /> <!--content sniffing-->
            </headers>
    
            <intercept-url pattern="/admin_side/**" access="hasRole('ROLE_ADMIN')" requires-channel="any"/>
            <form-login login-page="/admin_login/Login.action" authentication-success-handler-ref="loginSuccessHandler" authentication-failure-handler-ref="authenticationFailureHandler"/>
            <logout logout-success-url="/admin_login/Login.action" invalidate-session="true" delete-cookies="JSESSIONID"/>
        </http>
    
        <beans:bean id="encoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
    
        <beans:bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
            <beans:property name="userDetailsService" ref="userDetailsService"/>
            <beans:property name="passwordEncoder" ref="encoder" />
        </beans:bean>
    
        <beans:bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager">
            <beans:property name="providers">
                <beans:list>
                    <beans:ref bean="daoAuthenticationProvider" />
                </beans:list>
            </beans:property>
        </beans:bean>
    
        <authentication-manager>
            <authentication-provider user-service-ref="userDetailsService">
            </authentication-provider>
        </authentication-manager>
    
        <beans:bean id="loginSuccessHandler" class="loginsuccesshandler.LoginSuccessHandler"/>
        <beans:bean id="authenticationFailureHandler" class="loginsuccesshandler.AuthenticationFailureHandler" />
    
        <global-method-security secured-annotations="enabled" proxy-target-class="false" authentication-manager-ref="authenticationManager">
            <protect-pointcut expression="execution(* admin.dao.*.*(..))" access="ROLE_ADMIN"/>
        </global-method-security>
    </beans:beans>
    

    So, where to look for this token, when a request is multipart? (This should not be related to Struts at all.)

    The implementation of UserDetailsService can be found in this earlier question of mine, if needed.


    Placing MultipartFilter before Spring Security did not help either.

    The web.xml file looks like the following.

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app version="3.0"
             xmlns="http://java.sun.com/xml/ns/javaee" 
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
             http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    
        <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>
                /WEB-INF/applicationContext.xml
                /WEB-INF/spring-security.xml
            </param-value>
        </context-param>
    
        <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>
            <servlet-name>/*</servlet-name>
        </filter-mapping>
    
        <filter-mapping>
            <filter-name>springSecurityFilterChain</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
    
        <filter>
            <filter-name>AdminLoginNocacheFilter</filter-name>
            <filter-class>filter.AdminLoginNocacheFilter</filter-class>
        </filter>
    
        <filter-mapping>
            <filter-name>AdminLoginNocacheFilter</filter-name>
            <url-pattern>/admin_login/*</url-pattern>
        </filter-mapping>
    
        <filter>
            <filter-name>NoCacheFilter</filter-name>
            <filter-class>filter.NoCacheFilter</filter-class>
        </filter>
    
        <filter-mapping>
            <filter-name>NoCacheFilter</filter-name>
            <url-pattern>/admin_side/*</url-pattern>
        </filter-mapping>
    
        <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
        </listener>
    
        <listener>
            <description>Description</description>
            <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
        </listener>
    
        <listener>
            <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
        </listener>
    
        <filter>
            <filter-name>struts2</filter-name>
            <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
            <init-param>
                <param-name>struts.devMode</param-name>
                <param-value>true</param-value>
            </init-param>
        </filter>
    
        <filter-mapping>
            <filter-name>struts2</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
    
        <session-config>
            <session-timeout>
                30
            </session-timeout>
        </session-config>
        <welcome-file-list>
            <welcome-file>index.jsp</welcome-file>
        </welcome-file-list>
    </web-app>
    

    It only works, when the token is appended as a query-string parameter as follows which is however, discouraged.

    <s:form namespace="/admin_side"
            action="Category?%{#attr._csrf.parameterName}=%{#attr._csrf.token}"
            enctype="multipart/form-data"
            method="POST"
            validate="true"
            id="dataForm"
            name="dataForm">
        ...
    <s:form>