Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

File upload in Struts2 along with the Spring CSRF token

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.

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

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 is processed by Spring.

MultipartFilter in web.xml is configured as follows.

<?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>
        <url-pattern>/*</url-pattern>
    </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>

And in applicationContext.xml, MultipartResolver is registered as follows.

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

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

The CSRF token is now received by Spring security but doing so incurs another problem in Struts.

Uploaded file(s) is now null in Struts action classes like as follows.

@Namespace("/admin_side")
@ResultPath("/WEB-INF/content")
@ParentPackage(value="struts-default")
public final class CategoryAction extends ActionSupport implements Serializable, ValidationAware, ModelDriven<Category>
{
    private File fileUpload;
    private String fileUploadContentType;
    private String fileUploadFileName;
    private static final long serialVersionUID = 1L;

    //Getters and setters.

    //Necessary validators as required.
    @Action(value = "AddCategory",
        results = {
            @Result(name=ActionSupport.SUCCESS, type="redirectAction", params={"namespace", "/admin_side", "actionName", "Category"}),
            @Result(name = ActionSupport.INPUT, location = "Category.jsp")},
        interceptorRefs={
            @InterceptorRef(value="defaultStack", "validation.validateAnnotatedMethodOnly", "true"})
        })
    public String insert(){
        //fileUpload, fileUploadContentType and fileUploadFileName are null here after the form is submitted.
        return ActionSupport.SUCCESS;
    }

    @Action(value = "Category",
            results = {
                @Result(name=ActionSupport.SUCCESS, location="Category.jsp"),
                @Result(name = ActionSupport.INPUT, location = "Category.jsp")},
            interceptorRefs={
                @InterceptorRef(value="defaultStack", params={ "validation.validateAnnotatedMethodOnly", "true", "validation.excludeMethods", "load"})})
    public String load() throws Exception{
        //This method is just required to return an initial view on page load.
        return ActionSupport.SUCCESS;
    }
}

This happens because to my guess, the multipart request is already processed and consumed by Spring hence, it is not available to Struts as a multipart request and therefore, the file object in a Struts action class is null.

Is there a way to get around this situation? Otherwise, I have now left with the only option to append the token to a URL as a query-string parameter which is highly discouraged and not recommended at all.

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

Long story short : How to get files in a Struts action class, if Spring is made to process a mulipart request? On the other hand, if Spring is not made to process a multipart request then, it lakes the security token. How to overcome this situation?

like image 459
Tiny Avatar asked Feb 03 '14 17:02

Tiny


2 Answers

It seems your best bet is to create a custom MultiPartRequest implementation that delegates to Spring's MultipartRequest. Here is an example implementation:

sample/SpringMultipartParser.java

package sample;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map.Entry;

import javax.servlet.http.HttpServletRequest;

import org.apache.struts2.dispatcher.multipart.MultiPartRequest;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.util.WebUtils;

import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;

public class SpringMultipartParser implements MultiPartRequest {
    private static final Logger LOG = LoggerFactory.getLogger(MultiPartRequest.class);

    private List<String> errors = new ArrayList<String>();

    private MultiValueMap<String, MultipartFile> multipartMap;

    private MultipartHttpServletRequest multipartRequest;

    private MultiValueMap<String, File> multiFileMap = new LinkedMultiValueMap<String, File>();

    public void parse(HttpServletRequest request, String saveDir)
            throws IOException {
        multipartRequest =
                WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);

        if(multipartRequest == null) {
            LOG.warn("Unable to MultipartHttpServletRequest");
            errors.add("Unable to MultipartHttpServletRequest");
            return;
        }
        multipartMap = multipartRequest.getMultiFileMap();
        for(Entry<String, List<MultipartFile>> fileEntry : multipartMap.entrySet()) {
            String fieldName = fileEntry.getKey();
            for(MultipartFile file : fileEntry.getValue()) {
                File temp = File.createTempFile("upload", ".dat");
                file.transferTo(temp);
                multiFileMap.add(fieldName, temp);
            }
        }
    }

    public Enumeration<String> getFileParameterNames() {
        return Collections.enumeration(multipartMap.keySet());
    }

    public String[] getContentType(String fieldName) {
        List<MultipartFile> files = multipartMap.get(fieldName);
        if(files == null) {
            return null;
        }
        String[] contentTypes = new String[files.size()];
        int i = 0;
        for(MultipartFile file : files) {
            contentTypes[i++] = file.getContentType();
        }
        return contentTypes;
    }

    public File[] getFile(String fieldName) {
        List<File> files = multiFileMap.get(fieldName);
        return files == null ? null : files.toArray(new File[files.size()]);
    }

    public String[] getFileNames(String fieldName) {
        List<MultipartFile> files = multipartMap.get(fieldName);
        if(files == null) {
            return null;
        }
        String[] fileNames = new String[files.size()];
        int i = 0;
        for(MultipartFile file : files) {
            fileNames[i++] = file.getOriginalFilename();
        }
        return fileNames;
    }

    public String[] getFilesystemName(String fieldName) {
        List<File> files = multiFileMap.get(fieldName);
        if(files == null) {
            return null;
        }
        String[] fileNames = new String[files.size()];
        int i = 0;
        for(File file : files) {
            fileNames[i++] = file.getName();
        }
        return fileNames;
    }

    public String getParameter(String name) {
        return multipartRequest.getParameter(name);
    }

    public Enumeration<String> getParameterNames() {
        return multipartRequest.getParameterNames();
    }

    public String[] getParameterValues(String name) {
        return multipartRequest.getParameterValues(name);
    }

    public List getErrors() {
        return errors;
    }

    public void cleanUp() {
        for(List<File> files : multiFileMap.values()) {
            for(File file : files) {
                file.delete();
            }
        }

        // Spring takes care of the original File objects
    }
}

Next you need to ensure that Struts is using it. You can do this in your struts.xml file as shown below:

struts.xml

<constant name="struts.multipart.parser" value="spring"/>
<bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" 
      name="spring" 
      class="sample.SpringMultipartParser"
      scope="default"/>

WARNING: It is absolutely necessary to ensure that a new instance of MultipartRequest is created for every multipart request by properly setting the scope of the bean otherwise you will see race conditions.

After doing this, your Struts actions will have the file information added just as it was before. Keep in mind that validation of file (i.e. file size) is now done with filterMultipartResolver instead of Struts.

Using Themes to auto include the CSRF token

You might consider creating a custom theme so that you can automatically include the CSRF token in forms. For more information on how to do this see http://struts.apache.org/release/2.3.x/docs/themes-and-templates.html

Complete Example on Github

You can find a complete working sample on github at https://github.com/rwinch/struts2-upload

like image 156
Rob Winch Avatar answered Oct 09 '22 02:10

Rob Winch


The form encoding multipart/formdata is meant to be used for file upload scenarios, this is according to the W3C documentation:

The content type "multipart/form-data" should be used for submitting forms that contain files, non-ASCII data, and binary data.

The MultipartResolver class expects a file upload only, and not other form fields, this is from the javadoc:

/**
 * A strategy interface for multipart file upload resolution in accordance
 * with <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>.
 *
 */

So this is why adding the CSRF as a form field would not work, the usual way to secure file upload requests against CSRF attacks is to send the CSRF token in a HTTP request header instead of the POST body. For that you need to make it an ajax POST.

For a normal POST there is no way to do this, see this answer. Either make the POST an ajax request and add the header with some Javascript, or send the CSRF token as a URL parameter as you mentioned.

If the CSRF token is frequently regenerated as it should ideally be between requests, then sending it in as request parameter is less of a problem and might be acceptable.

On the server side, you would need to configure the CSRF solution to read the token from the header, this is usually foreseen by the CSRF solution being used.

like image 42
Angular University Avatar answered Oct 09 '22 03:10

Angular University