File upload in Struts2 along with the Spring CSRF token File upload in Struts2 along with the Spring CSRF token spring spring

File upload in Struts2 along with the Spring CSRF token


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


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.


At a first glance your configuration looks correct to me. I therefore think that the problem might be some tiny bit of misconfiguration somewhere.

I faced a similar problem with Spring MVC instead of Struts, which I was able to solve with help from the Spring Security team. For full details see this answer.

You may also compare your set up with a working sample available on Github. I have tested this on Tomcat 7, JBoss AS 7, Jetty and Weblogic.

If these do not work, it will be helpful if you can create a single controller, single page application with your configuration that demonstrates the problem and upload it somewhere.