Parse correctly ContentDisposition header with semicolons

Issue: SPR-16091
master
Sebastien Deleuze 7 years ago
parent a3e62284ea
commit 1c256a112b
  1. 47
      spring-web/src/main/java/org/springframework/http/ContentDisposition.java
  2. 16
      spring-web/src/test/java/org/springframework/http/ContentDispositionTests.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<String> 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<String> 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<String> 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.
* <p>Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported.

@ -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

Loading…
Cancel
Save