From 1c256a112b345f1a35a572c7498abc3f95b997ea Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 23 Oct 2017 22:59:25 +0200 Subject: [PATCH] Parse correctly ContentDisposition header with semicolons Issue: SPR-16091 --- .../http/ContentDisposition.java | 47 ++++++++++++++++--- .../http/ContentDispositionTests.java | 16 +++++++ 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java index 7554373e82..acbae702dc 100644 --- a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java +++ b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java @@ -21,11 +21,12 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.ZonedDateTime; import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; import static java.nio.charset.StandardCharsets.*; import static java.time.format.DateTimeFormatter.*; @@ -253,9 +254,8 @@ public class ContentDisposition { * @see #toString() */ public static ContentDisposition parse(String contentDisposition) { - String[] parts = StringUtils.tokenizeToStringArray(contentDisposition, ";"); - Assert.isTrue(parts.length >= 1, "Content-Disposition header must not be empty"); - String type = parts[0]; + List parts = tokenize(contentDisposition); + String type = parts.get(0); String name = null; String filename = null; Charset charset = null; @@ -263,8 +263,8 @@ public class ContentDisposition { ZonedDateTime creationDate = null; ZonedDateTime modificationDate = null; ZonedDateTime readDate = null; - for (int i = 1; i < parts.length; i++) { - String part = parts[i]; + for (int i = 1; i < parts.size(); i++) { + String part = parts.get(i); int eqIndex = part.indexOf('='); if (eqIndex != -1) { String attribute = part.substring(0, eqIndex); @@ -318,6 +318,41 @@ public class ContentDisposition { return new ContentDisposition(type, name, filename, charset, size, creationDate, modificationDate, readDate); } + private static List tokenize(String headerValue) { + int index = headerValue.indexOf(';'); + String type = (index >= 0 ? headerValue.substring(0, index) : headerValue).trim(); + if (type.isEmpty()) { + throw new IllegalArgumentException("Content-Disposition header must not be empty"); + } + List parts = new ArrayList<>(); + parts.add(type); + if (index >= 0) { + do { + int nextIndex = index + 1; + boolean quoted = false; + while (nextIndex < headerValue.length()) { + char ch = headerValue.charAt(nextIndex); + if (ch == ';') { + if (!quoted) { + break; + } + } + else if (ch == '"') { + quoted = !quoted; + } + nextIndex++; + } + String part = headerValue.substring(index + 1, nextIndex).trim(); + if (!part.isEmpty()) { + parts.add(part); + } + index = nextIndex; + } + while (index < headerValue.length()); + } + return parts; + } + /** * Decode the given header field param as describe in RFC 5987. *

Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported. diff --git a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java index df0662d506..bd6990a6c1 100644 --- a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java +++ b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java @@ -55,6 +55,22 @@ public class ContentDispositionTests { assertEquals(ContentDisposition.builder("form-data").filename("unquoted").build(), disposition); } + @Test // SPR-16091 + public void parseFilenameWithSemicolon() { + ContentDisposition disposition = ContentDisposition + .parse("attachment; filename=\"filename with ; semicolon.txt\""); + assertEquals(ContentDisposition.builder("attachment") + .filename("filename with ; semicolon.txt").build(), disposition); + } + + @Test + public void parseAndIgnoreEmptyParts() { + ContentDisposition disposition = ContentDisposition + .parse("form-data; name=\"foo\";; ; filename=\"foo.txt\"; size=123"); + assertEquals(ContentDisposition.builder("form-data") + .name("foo").filename("foo.txt").size(123L).build(), disposition); + } + @Test public void parseEncodedFilename() { ContentDisposition disposition = ContentDisposition