Skip to content

Commit 1c256a1

Browse files
committed
Parse correctly ContentDisposition header with semicolons
Issue: SPR-16091
1 parent a3e6228 commit 1c256a1

File tree

2 files changed

+57
-6
lines changed

2 files changed

+57
-6
lines changed

spring-web/src/main/java/org/springframework/http/ContentDisposition.java

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@
2121
import java.nio.charset.StandardCharsets;
2222
import java.time.ZonedDateTime;
2323
import java.time.format.DateTimeParseException;
24+
import java.util.ArrayList;
25+
import java.util.List;
2426

2527
import org.springframework.lang.Nullable;
2628
import org.springframework.util.Assert;
2729
import org.springframework.util.ObjectUtils;
28-
import org.springframework.util.StringUtils;
2930

3031
import static java.nio.charset.StandardCharsets.*;
3132
import static java.time.format.DateTimeFormatter.*;
@@ -253,18 +254,17 @@ public static ContentDisposition empty() {
253254
* @see #toString()
254255
*/
255256
public static ContentDisposition parse(String contentDisposition) {
256-
String[] parts = StringUtils.tokenizeToStringArray(contentDisposition, ";");
257-
Assert.isTrue(parts.length >= 1, "Content-Disposition header must not be empty");
258-
String type = parts[0];
257+
List<String> parts = tokenize(contentDisposition);
258+
String type = parts.get(0);
259259
String name = null;
260260
String filename = null;
261261
Charset charset = null;
262262
Long size = null;
263263
ZonedDateTime creationDate = null;
264264
ZonedDateTime modificationDate = null;
265265
ZonedDateTime readDate = null;
266-
for (int i = 1; i < parts.length; i++) {
267-
String part = parts[i];
266+
for (int i = 1; i < parts.size(); i++) {
267+
String part = parts.get(i);
268268
int eqIndex = part.indexOf('=');
269269
if (eqIndex != -1) {
270270
String attribute = part.substring(0, eqIndex);
@@ -318,6 +318,41 @@ else if (attribute.equals("read-date")) {
318318
return new ContentDisposition(type, name, filename, charset, size, creationDate, modificationDate, readDate);
319319
}
320320

321+
private static List<String> tokenize(String headerValue) {
322+
int index = headerValue.indexOf(';');
323+
String type = (index >= 0 ? headerValue.substring(0, index) : headerValue).trim();
324+
if (type.isEmpty()) {
325+
throw new IllegalArgumentException("Content-Disposition header must not be empty");
326+
}
327+
List<String> parts = new ArrayList<>();
328+
parts.add(type);
329+
if (index >= 0) {
330+
do {
331+
int nextIndex = index + 1;
332+
boolean quoted = false;
333+
while (nextIndex < headerValue.length()) {
334+
char ch = headerValue.charAt(nextIndex);
335+
if (ch == ';') {
336+
if (!quoted) {
337+
break;
338+
}
339+
}
340+
else if (ch == '"') {
341+
quoted = !quoted;
342+
}
343+
nextIndex++;
344+
}
345+
String part = headerValue.substring(index + 1, nextIndex).trim();
346+
if (!part.isEmpty()) {
347+
parts.add(part);
348+
}
349+
index = nextIndex;
350+
}
351+
while (index < headerValue.length());
352+
}
353+
return parts;
354+
}
355+
321356
/**
322357
* Decode the given header field param as describe in RFC 5987.
323358
* <p>Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported.

spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,22 @@ public void parseUnquotedFilename() {
5555
assertEquals(ContentDisposition.builder("form-data").filename("unquoted").build(), disposition);
5656
}
5757

58+
@Test // SPR-16091
59+
public void parseFilenameWithSemicolon() {
60+
ContentDisposition disposition = ContentDisposition
61+
.parse("attachment; filename=\"filename with ; semicolon.txt\"");
62+
assertEquals(ContentDisposition.builder("attachment")
63+
.filename("filename with ; semicolon.txt").build(), disposition);
64+
}
65+
66+
@Test
67+
public void parseAndIgnoreEmptyParts() {
68+
ContentDisposition disposition = ContentDisposition
69+
.parse("form-data; name=\"foo\";; ; filename=\"foo.txt\"; size=123");
70+
assertEquals(ContentDisposition.builder("form-data")
71+
.name("foo").filename("foo.txt").size(123L).build(), disposition);
72+
}
73+
5874
@Test
5975
public void parseEncodedFilename() {
6076
ContentDisposition disposition = ContentDisposition

0 commit comments

Comments
 (0)