20

I'm using Spring Web 4.0.5, Spring Security 3.2.4, Commons FileUpload 1.3.1, Tomcat 7 and I'm getting an ugly MaxUploadSizeExceededException when my upload size limit is exceeded, which results in a "500 Internal Server Error". I handle it with a nice generic popup, but I'd rather have my Controller take care of it by going back to the originating form with the proper explanation message.

I've seen a similar question asked many times, with a few solutions that might work when not using Spring Security; none of the ones I tried worked for me.

The problem might be that when using Spring Security, the CommonsMultipartResolver is not added as a "multipartResolver" bean but as a "filterMultipartResolver":

@Bean(name="filterMultipartResolver")
CommonsMultipartResolver filterMultipartResolver() {
    CommonsMultipartResolver filterMultipartResolver = new CommonsMultipartResolver();
    filterMultipartResolver.setMaxUploadSize(MAXSIZE);
    return filterMultipartResolver;
}

If I set filterMultipartResolver.setResolveLazily(true); it makes no difference.

If I subclass the CommonsMultipartResolver with my own and override the parseRequest() method with something that traps the MaxUploadSizeExceededException and returns an empty MultipartParsingResult, I get a "403 Forbidden" error:

public class ExtendedCommonsMultipartResolver extends CommonsMultipartResolver {
    protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
        String encoding = determineEncoding(request);
        try {
            return super.parseRequest(request);
        } catch (MaxUploadSizeExceededException e) {
            return parseFileItems(Collections.<FileItem> emptyList(), encoding);
        }
    }
}

Finally, there's no point in implementing some kind of local or global ExceptionHandler because it is never called.

If I don't find a better solution, I'll just remove the upload size limit and handle it myself in the controller, with the drawback of having the user wait until the upload is finished before seeing the error message about file size. Of I might even ignore all of this because, being it an image in this case, I could just resize it down to proper values.

Still, I'd like to see a solution to this problem.

Thank you

EDIT:

I add the stack trace as requested. This is the case where a 500 is generated.

May 30, 2014 12:47:17 PM org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet [dispatcher] in context with path [/site] threw exception
org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size of 1000000 bytes exceeded; nested exception is org.apache.commons.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (3403852) exceeds the configured maximum (1000000)
    at org.springframework.web.multipart.commons.CommonsMultipartResolver.parseRequest(CommonsMultipartResolver.java:162)
    at org.springframework.web.multipart.commons.CommonsMultipartResolver.resolveMultipart(CommonsMultipartResolver.java:142)
    at org.springframework.web.multipart.support.MultipartFilter.doFilterInternal(MultipartFilter.java:110)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:100)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:953)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:409)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1044)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:607)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:315)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
    at java.lang.Thread.run(Thread.java:722)
Caused by: org.apache.commons.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (3403852) exceeds the configured maximum (1000000)
    at org.apache.commons.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:965)
    at org.apache.commons.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:310)
    at org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:334)
    at org.apache.commons.fileupload.servlet.ServletFileUpload.parseRequest(ServletFileUpload.java:115)
    at org.springframework.web.multipart.commons.CommonsMultipartResolver.parseRequest(CommonsMultipartResolver.java:158)
    ... 19 more
xtian
  • 2,908
  • 2
  • 30
  • 43
  • Please post your full stacktraces – Rob Winch May 27 '14 at 15:31
  • Some further investigation reveales that, when using the only viable approach that is the ExtendedCommonsMultipartResolver, the "403 Forbidden" is caused by the csrf feature of Spring Security. If I disable it with http.csrf().disable() in my WebSecurityConfigurerAdapter, the 403 is not generated. Of course I'd prefer not to disable it, not even partially with a requireCsrfProtectionMatcher() because I wouldn't like my users to upload someone else's data, unless the csrf trick can't be played with a multipart. The original question still holds. Please advise. – xtian May 30 '14 at 09:53
  • I have the feeling that the matter can not be solved unless A) I set the _csrf on the url as explained here http://docs.spring.io/spring-security/site/docs/3.2.0.CI-SNAPSHOT/reference/html/csrf.html#csrf-multipartfilter or B) I write an ExtendedCommonsMultipartResolver that traps the MaxUploadSizeExceededException and repeats the parseRequest after disabling the size check and somehow flags the excess. Option B would still load the whole file so it's pointless. I guess we really have one option. – xtian May 30 '14 at 11:01

4 Answers4

12

You can handle the MaxUploadSizeExceededException by adding an additional Filter to catch the exception and the redirect to an error page. For example, you could create a MultipartExceptionHandler Filter like the following:

public class MultipartExceptionHandler extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } catch (MaxUploadSizeExceededException e) {
            handle(request, response, e);
        } catch (ServletException e) {
            if(e.getRootCause() instanceof MaxUploadSizeExceededException) {
                handle(request, response, (MaxUploadSizeExceededException) e.getRootCause());
            } else {
                throw e;
            }
        }
    }

    private void handle(HttpServletRequest request,
            HttpServletResponse response, MaxUploadSizeExceededException e) throws ServletException, IOException {

        String redirect = UrlUtils.buildFullRequestUrl(request) + "?error";
        response.sendRedirect(redirect);
    }

}

NOTE: This redirect makes an assumption about your form and upload. You may need to modify where to redirect to. Specifically if you follow the pattern of your form being at GET and it is processed at POST this will work.

You can then ensure to add this Filter before MultipartFilter. For example, if you are using web.xml you would see something like this:

<filter>
    <filter-name>meh</filter-name>
    <filter-class>org.example.web.MultipartExceptionHandler</filter-class>
</filter>
<filter>
    <description>
        Allows the application to accept multipart file data.
    </description>
    <display-name>springMultipartFilter</display-name>
    <filter-name>springMultipartFilter</filter-name>
    <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
    <!--init-param>
        <param-name>multipartResolverBeanName</param-name>
        <param-value>multipartResolver</param-value>
    </init-param-->
</filter>
<filter>
    <description>
        Secures access to web resources using the Spring Security framework.
    </description>
    <display-name>springSecurityFilterChain</display-name>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
    <filter-name>meh</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>springMultipartFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>ERROR</dispatcher>
    <dispatcher>REQUEST</dispatcher>
</filter-mapping>

In your form you can then detect if the error occurred by inspecting if the HTTP parameter error is present. For example, in a JSP you might do the following:

<c:if test="${param.error != null}">
    <p>Failed to upload...too big</p>
</c:if>

PS: I created SEC-2614 to update the documentation to discuss error handling

Rob Winch
  • 21,440
  • 2
  • 59
  • 76
4

I know I'm late to the party, but I found a much more elegant solution imho.

Instead of adding a filter for the multipart resolver, simply add throws MaxUploadSizeExceededException on your controller method and add the filter for the DelegatingFilterProxy in your web.xml and you can add an exception handler right in your controller without having to redirect the request.

e.g.:

Method (in Controller):

@RequestMapping(value = "/uploadFile", method = RequestMethod.POST)
public ResponseEntity<String> uploadFile(MultipartHttpServletRequest request) throws MaxUploadSizeExceededException {
    //code
}

Exception Handler (in same controller):

@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity handleSizeExceededException(HttpServletRequest request, Exception ex) {
    //code
}

Web.xml (thanks to Rob Winch):

<filter>
    <description>
        Secures access to web resources using the Spring Security framework.
    </description>
    <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>REQUEST</dispatcher>
</filter-mapping>

And that is all you need.

Falko
  • 17,076
  • 13
  • 60
  • 105
Taugenichts
  • 1,307
  • 12
  • 19
  • On startup i get following error "org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'springSecurityFilterChain' is defined" – DCO Mar 08 '17 at 12:11
1

The thing is springSecurityFilterChain must be added after multipart filter. That's why you are getting 403 status. Here:

http://docs.spring.io/spring-security/site/docs/3.2.0.CI-SNAPSHOT/reference/html/csrf.html#csrf-multipartfilter

I think after you do so, you will be able to catch FileUploadBase.SizeLimitExceededException in a @ControllerAdvice annotated class containing @ExceptionHandler annotated methods.

px5x2
  • 1,495
  • 2
  • 14
  • 29
  • I already did that. I think that without it the file upload wouldn't work ever; I only have a problem when the file limit is exceeded. By the way, the documentation to which you refer seems to be wrong: "Specifying the MultipartFilter AFTER the Spring Security filter means that there is no authorization" should read "Specifying the MultipartFilter BEFORE" in my opinion. – xtian May 30 '14 at 10:39
  • You are right the MultipartFilter should be before springSecurityFilterChain. This appears to be what was stated in the answer & how it is documented in the provided link. If I am missing something wrong in the doc please point it out and I will be sure to fix it. – Rob Winch May 30 '14 at 13:57
  • Sorry I wasn't clear. The provided link correctly says that the MultipartFilter should be before, but it then goes on saying that when the MultipartFilter is after, there is no authorization. Here, it should say "before", because when the MultipartFilter is before the Security filter, which is the case in question, obviously it is not protected by authorization. – xtian May 30 '14 at 14:51
0

The solution I came up with while experimenting is the following:

  1. Extend CommonsMultipartResolver in order to swallow the exception. I add the exception to the Request just in case you want to use it in the Controller, but I don't think it's needed

    package org.springframework.web.multipart.commons;
    
    import java.util.Collections;
    
    import javax.servlet.http.HttpServletRequest;
    
    import org.apache.commons.fileupload.FileItem;
    import org.springframework.web.multipart.MaxUploadSizeExceededException;
    import org.springframework.web.multipart.MultipartException;
    
    public class ExtendedCommonsMultipartResolver extends CommonsMultipartResolver {
        @Override
        protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
            try {
                return super.parseRequest(request);
            } catch (MaxUploadSizeExceededException e) {
                request.setAttribute("MaxUploadSizeExceededException", e);
                return parseFileItems(Collections.<FileItem> emptyList(), null);
            }
        }
    }
    
  2. Declare your resolver in the WebSecurityConfigurerAdapter, in place of CommonsMultipartResolver (you should declare a filterMultipartResolver in any case so nothing new here)

    @Bean(name="filterMultipartResolver")
    CommonsMultipartResolver filterMultipartResolver() {
        CommonsMultipartResolver filterMultipartResolver = new ExtendedCommonsMultipartResolver();
        filterMultipartResolver.setMaxUploadSize(MAXBYTES);
        return filterMultipartResolver;
    }
    
  3. Remember to define the correct filter precedence in the AbstractSecurityWebApplicationInitializer as stated in the docs (you'd do this in any case)

    @Order(1)
    public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
        @Override
        protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
            insertFilters(servletContext, new MultipartFilter());
        }
    }
    
  4. Add the _csrf token to the form action URL (I'm using thymeleaf here)

    <form th:action="@{|/submitImage?${_csrf.parameterName}=${_csrf.token}|}" 
    
  5. In the Controller, just check for null on the MultipartFile, something like (snippet not checked for errors):

    @RequestMapping(value = "/submitImage", method = RequestMethod.POST)
    public String submitImage(MyFormBean myFormBean, BindingResult bindingResult, HttpServletRequest request, Model model) {
        MultipartFile multipartFile = myFormBean.getImage();
        if (multipartFile==null) {
            bindingResult.rejectValue("image", "validation.image.filesize");
        } else if (multipartFile.isEmpty()) {
            bindingResult.rejectValue("image", "validation.image.missing");
    

This way you can use the usual Controller method for handling the form submission even in case of size exceeded.

What I don't like of this approach is that you have to mess with an external library package (MultipartParsingResult is protected) and that you have to remember setting the token on the form url (which is also less secure btw).

What I do like is that you handle the form submission in just one place in the controller.

The problem of a big file being fully downloaded before returning to the user also persists, but I guess it is addressed somewhere else already.

xtian
  • 2,908
  • 2
  • 30
  • 43