I would like to be able to upload images to a server, handling errors and exceptions gracefully, with error messages displayed to the user in the form, and ideally only using an existing barebones Spring Boot and Thymeleaf install.
Using the example project gs-uploading-files I can upload files to a server using Spring Boot and Thymeleaf. In application.properties I set spring.http.multipart.max-file-size=1MB
and
spring.http.multipart.max-request-size=1MB
.
However several security and validation issues are unresolved when I upload files larger than 1MB.
Any file can be uploaded. For example, an html file could be uploaded and thus hosted on the server. How can files be restricted by type? Can they be validated in the page before the request is sent? If I have multiple ways of uploading images how can I validate all MultipartFiles?
Users can attempt to uploade large files, beyond the default limits of Spring and the embedded Tomcat. This causes a org.springframework.web.multipart.MultipartException
is not handled by Spring. How can the file size be validated before the upload attempt? In case this is bypassesed can any file upload exceptions caught by Spring, so that a nice error message is displayed?
The default Spring error page is not used as a fallback for all exceptions. The MultipartException returns a Tomcat exception page with a full stacktrace (see Log 1).
I've searched to try and find and implement a set of solutions.
A step towards fixing number 1 is by amending to handleFileUpload
check the content type, rejecting files which fail this: !file.getContentType().toLowerCase().startsWith("image")
. Will this always be valid? Can a malicious user bypass this? And how can I check every MultipartFile, to save having to remember to add this every time?
@PostMapping("/")
public String handleFileUpload(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes)
throws MultipartException, IllegalStateException {
if (file != null && file.getContentType() != null && !file.getContentType().toLowerCase().startsWith("image"))
throw new MultipartException("not img");
storageService.store(file);
redirectAttributes.addFlashAttribute("message",
"You successfully uploaded " + file.getOriginalFilename() + "!");
return "redirect:/";
}
Adding an @ExceptionHandler
doesn't work, it simply never gets called.
@ExceptionHandler({ SizeLimitExceededException.class, MultipartException.class,
java.lang.IllegalStateException.class })
public ModelAndView handleError(HttpServletRequest req, Exception e) {
// error("Request: " + req.getRequestURL() + " raised " + ex);
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", req.getRequestURL());
mav.addObject("timestamp", new Date());
mav.addObject("error", e.getClass());
mav.addObject("message", e.getMessage());
mav.addObject("status", HttpStatus.INTERNAL_SERVER_ERROR);
mav.setViewName("error");
return mav;
}
Number 3 can be resolved by a global exception handler on all Exceptions. (explained in detail in this post). However, I'm concerned that it may overrule a Controller level handler.
package hello;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;
@ControllerAdvice
class GlobalDefaultExceptionHandler {
public static final String DEFAULT_ERROR_VIEW = "error";
@ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
// If the exception is annotated with @ResponseStatus rethrow it and let
// the framework handle it - like the OrderNotFoundException example
// at the start of this post.
// AnnotationUtils is a Spring Framework utility class.
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null)
throw e;
// Otherwise setup and send the user to a default error-view.
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", req.getRequestURL());
mav.addObject("timestamp", new Date());
mav.addObject("error", e.getClass());
mav.addObject("message", e.getMessage());
mav.addObject("status", HttpStatus.INTERNAL_SERVER_ERROR);
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}
}
I've attempted this answer, which handles the exception but returns an error page. I'd like to return to the original page and display a nice error message.
Log 1:
HTTP Status 500 - Request processing failed; nested exception is org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)
type Exception report
message Request processing failed; nested exception is org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)
description The server encountered an internal error that prevented it from fulfilling this request.
exception
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:982)
org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
javax.servlet.http.HttpServlet.service(HttpServlet.java:648)
org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:89)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
root cause
org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)
org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:111)
org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:85)
org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:76)
org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1099)
org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:932)
org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:897)
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
javax.servlet.http.HttpServlet.service(HttpServlet.java:648)
org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:89)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
root cause
java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)
org.apache.catalina.connector.Request.parseParts(Request.java:2871)
org.apache.catalina.connector.Request.parseParameters(Request.java:3176)
org.apache.catalina.connector.Request.getParameter(Request.java:1110)
org.apache.catalina.connector.RequestFacade.getParameter(RequestFacade.java:381)
org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:70)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
root cause
org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1292555) exceeds the configured maximum (1048576)
org.apache.tomcat.util.http.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:811)
org.apache.tomcat.util.http.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:256)
org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:280)
org.apache.catalina.connector.Request.parseParts(Request.java:2801)
org.apache.catalina.connector.Request.parseParameters(Request.java:3176)
org.apache.catalina.connector.Request.getParameter(Request.java:1110)
org.apache.catalina.connector.RequestFacade.getParameter(RequestFacade.java:381)
org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:70)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
note The full stack trace of the root cause is available in the Apache Tomcat/8.5.5 logs.
Apache Tomcat/8.5.5
To reply on how to check file types: I have created a custom validator for this.
First, create an annotation:
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {ImageFileValidator.class})
public @interface ValidImage {
String message() default "Invalid image file";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Next, create the validator itself:
import org.springframework.web.multipart.MultipartFile;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class ImageFileValidator implements ConstraintValidator<ValidImage, MultipartFile> {
@Override
public void initialize(ValidImage constraintAnnotation) {
}
@Override
public boolean isValid(MultipartFile multipartFile, ConstraintValidatorContext context) {
boolean result = true;
String contentType = multipartFile.getContentType();
if (!isSupportedContentType(contentType)) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
"Only PNG or JPG images are allowed.")
.addConstraintViolation();
result = false;
}
return result;
}
private boolean isSupportedContentType(String contentType) {
return contentType.equals("image/png")
|| contentType.equals("image/jpg")
|| contentType.equals("image/jpeg");
}
}
Finally, apply the annotation:
public class CreateUserParameters {
@NotNull
@ValidImage
private MultipartFile image;
...
}
I have tested this with Spring Boot 1.5.10 (Also with Thymeleaf)
For the max file size, I would also like to see a solution that works with the "standard error mechanism" so you can display the error like other field errors and the user can correct his mistake.
Try adding the following in your application.properties to set the file size limit:
spring.http.multipart.max-file-size=256KB
spring.http.multipart.max-request-size=256KB
Source: https://spring.io/guides/gs/uploading-files/
EDIT: Since Spring boot 2.0 release, the property names have changed to:
spring.servlet.multipart.max-file-size=128KB
spring.servlet.multipart.max-request-size=128KB
Note the difference spring.http.
--> spring.servlet.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With