From bb684ce60b82bb2d3a81828a6780800c51bb4e33 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 20 Jul 2017 14:21:10 +0200 Subject: [PATCH] Improve decoding support for multipart filename StandardMultipartHttpServletRequest now properly decodes RFC-5987 encoded filenames (i.e. filename*) by delegating to ContentDisposition and also support RFC-2047 syntax through javax.mail MimeUtility. Issue: SPR-15205 --- .../StandardMultipartHttpServletRequest.java | 95 +++++-------------- ...ndardMultipartHttpServletRequestTests.java | 76 +++++++++++++++ .../RequestPartIntegrationTests.java | 2 +- 3 files changed, 102 insertions(+), 71 deletions(-) create mode 100644 spring-web/src/test/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequestTests.java diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java b/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java index 655c9f07cdc..f70dcab9bd0 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java @@ -20,8 +20,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; +import java.io.UnsupportedEncodingException; import java.nio.file.Files; import java.util.ArrayList; import java.util.Collection; @@ -31,9 +30,11 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import javax.mail.internet.MimeUtility; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.Part; +import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.lang.Nullable; import org.springframework.util.FileCopyUtils; @@ -54,13 +55,6 @@ import org.springframework.web.multipart.MultipartFile; */ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest { - private static final String CONTENT_DISPOSITION = "content-disposition"; - - private static final String FILENAME_KEY = "filename="; - - private static final String FILENAME_WITH_CHARSET_KEY = "filename*="; - - @Nullable private Set multipartParameterNames; @@ -96,12 +90,13 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe this.multipartParameterNames = new LinkedHashSet<>(parts.size()); MultiValueMap files = new LinkedMultiValueMap<>(parts.size()); for (Part part : parts) { - String disposition = part.getHeader(CONTENT_DISPOSITION); - String filename = extractFilename(disposition); - if (filename == null) { - filename = extractFilenameWithCharset(disposition); - } + String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION); + ContentDisposition disposition = ContentDisposition.parse(headerValue); + String filename = disposition.getFilename(); if (filename != null) { + if (filename.startsWith("=?") && filename.endsWith("?=")) { + filename = MimeDelegate.decode(filename); + } files.add(part.getName(), new StandardMultipartFile(part, filename)); } else { @@ -123,62 +118,6 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe throw new MultipartException("Failed to parse multipart servlet request", ex); } - @Nullable - private String extractFilename(String contentDisposition, String key) { - int startIndex = contentDisposition.indexOf(key); - if (startIndex == -1) { - return null; - } - String filename = contentDisposition.substring(startIndex + key.length()); - if (filename.startsWith("\"")) { - int endIndex = filename.indexOf("\"", 1); - if (endIndex != -1) { - return filename.substring(1, endIndex); - } - } - else { - int endIndex = filename.indexOf(";"); - if (endIndex != -1) { - return filename.substring(0, endIndex); - } - } - return filename; - } - - @Nullable - private String extractFilename(String contentDisposition) { - return extractFilename(contentDisposition, FILENAME_KEY); - } - - @Nullable - private String extractFilenameWithCharset(String contentDisposition) { - String filename = extractFilename(contentDisposition, FILENAME_WITH_CHARSET_KEY); - if (filename == null) { - return null; - } - int index = filename.indexOf("'"); - if (index != -1) { - Charset charset = null; - try { - charset = Charset.forName(filename.substring(0, index)); - } - catch (IllegalArgumentException ex) { - // ignore - } - filename = filename.substring(index + 1); - // Skip language information.. - index = filename.indexOf("'"); - if (index != -1) { - filename = filename.substring(index + 1); - } - if (charset != null) { - filename = new String(filename.getBytes(StandardCharsets.US_ASCII), charset); - } - } - return filename; - } - - @Override protected void initializeMultipart() { parseRequest(getRequest()); @@ -322,4 +261,20 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe } } + + /** + * Inner class to avoid a hard dependency on the JavaMail API. + */ + private static class MimeDelegate { + + public static String decode(String value) { + try { + return MimeUtility.decodeText(value); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException(ex); + } + } + } + } diff --git a/spring-web/src/test/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequestTests.java b/spring-web/src/test/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequestTests.java new file mode 100644 index 00000000000..f8f56cd6325 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequestTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.multipart.support; + +import org.junit.Test; + +import org.springframework.mock.web.test.MockHttpServletRequest; +import org.springframework.mock.web.test.MockPart; +import org.springframework.web.multipart.MultipartFile; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Unit tests for {@link StandardMultipartHttpServletRequest}. + * @author Rossen Stoyanchev + */ +public class StandardMultipartHttpServletRequestTests { + + + @Test + public void filename() throws Exception { + + StandardMultipartHttpServletRequest request = getRequest( + "file", "form-data; name=\"file\"; filename=\"myFile.txt\""); + + MultipartFile multipartFile = request.getFile("file"); + assertNotNull(multipartFile); + assertEquals("myFile.txt", multipartFile.getOriginalFilename()); + } + + @Test // SPR-13319 + public void filenameRfc5987() throws Exception { + + StandardMultipartHttpServletRequest request = getRequest( + "file", "form-data; name=\"file\"; filename*=\"UTF-8''foo-%c3%a4-%e2%82%ac.html\""); + + MultipartFile multipartFile = request.getFile("file"); + assertNotNull(multipartFile); + assertEquals("foo-ä-€.html", multipartFile.getOriginalFilename()); + } + + @Test // SPR-15205 + public void filenameRfc2047() throws Exception { + + StandardMultipartHttpServletRequest request = getRequest( + "file", "form-data; name=\"file\"; filename=\"=?UTF-8?Q?Declara=C3=A7=C3=A3o.pdf?=\""); + + MultipartFile multipartFile = request.getFile("file"); + assertNotNull(multipartFile); + assertEquals("Declaração.pdf", multipartFile.getOriginalFilename()); + } + + + private StandardMultipartHttpServletRequest getRequest(String name, String dispositionValue) { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockPart part = new MockPart(name, new byte[0]); + part.getHeaders().set("Content-Disposition", dispositionValue); + request.addPart(part); + return new StandardMultipartHttpServletRequest(request); + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java index 15b7da41e5e..961c4ab346d 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java @@ -249,7 +249,7 @@ public class RequestPartIntegrationTests { @RequestMapping(value = "/spr13319", method = POST, consumes = "multipart/form-data") public ResponseEntity create(@RequestPart("file") MultipartFile multipartFile) { - assertEquals("%C3%A9l%C3%A8ve.txt", multipartFile.getOriginalFilename()); + assertEquals("élève.txt", multipartFile.getOriginalFilename()); return ResponseEntity.ok().build(); } }