Skip to content

Commit 0583b33

Browse files
committed
Escape quotes in filename
Also sync up to master and 5.1.x on refactorings in ContentDisposition and ContentDispositionTests. Closes spring-projectsgh-24230
1 parent 8356762 commit 0583b33

File tree

2 files changed

+219
-133
lines changed

2 files changed

+219
-133
lines changed

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

Lines changed: 71 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,8 +28,9 @@
2828
import org.springframework.util.Assert;
2929
import org.springframework.util.ObjectUtils;
3030

31-
import static java.nio.charset.StandardCharsets.*;
32-
import static java.time.format.DateTimeFormatter.*;
31+
import static java.nio.charset.StandardCharsets.ISO_8859_1;
32+
import static java.nio.charset.StandardCharsets.UTF_8;
33+
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
3334

3435
/**
3536
* Represent the Content-Disposition type and parameters as defined in RFC 2183.
@@ -39,7 +40,11 @@
3940
* @since 5.0
4041
* @see <a href="https://tools.ietf.org/html/rfc2183">RFC 2183</a>
4142
*/
42-
public class ContentDisposition {
43+
public final class ContentDisposition {
44+
45+
private static final String INVALID_HEADER_FIELD_PARAMETER_FORMAT =
46+
"Invalid header field parameter format (as defined in RFC 5987)";
47+
4348

4449
@Nullable
4550
private final String type;
@@ -200,11 +205,11 @@ public String toString() {
200205
if (this.filename != null) {
201206
if (this.charset == null || StandardCharsets.US_ASCII.equals(this.charset)) {
202207
sb.append("; filename=\"");
203-
sb.append(this.filename).append('\"');
208+
sb.append(escapeQuotationsInFilename(this.filename)).append('\"');
204209
}
205210
else {
206211
sb.append("; filename*=");
207-
sb.append(encodeHeaderFieldParam(this.filename, this.charset));
212+
sb.append(encodeFilename(this.filename, this.charset));
208213
}
209214
}
210215
if (this.size != null) {
@@ -270,15 +275,23 @@ public static ContentDisposition parse(String contentDisposition) {
270275
String attribute = part.substring(0, eqIndex);
271276
String value = (part.startsWith("\"", eqIndex + 1) && part.endsWith("\"") ?
272277
part.substring(eqIndex + 2, part.length() - 1) :
273-
part.substring(eqIndex + 1, part.length()));
278+
part.substring(eqIndex + 1));
274279
if (attribute.equals("name") ) {
275280
name = value;
276281
}
277282
else if (attribute.equals("filename*") ) {
278-
filename = decodeHeaderFieldParam(value);
279-
charset = Charset.forName(value.substring(0, value.indexOf('\'')));
280-
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
281-
"Charset should be UTF-8 or ISO-8859-1");
283+
int idx1 = value.indexOf('\'');
284+
int idx2 = value.indexOf('\'', idx1 + 1);
285+
if (idx1 != -1 && idx2 != -1) {
286+
charset = Charset.forName(value.substring(0, idx1).trim());
287+
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
288+
"Charset should be UTF-8 or ISO-8859-1");
289+
filename = decodeFilename(value.substring(idx2 + 1), charset);
290+
}
291+
else {
292+
// US ASCII
293+
filename = decodeFilename(value, StandardCharsets.US_ASCII);
294+
}
282295
}
283296
else if (attribute.equals("filename") && (filename == null)) {
284297
filename = value;
@@ -330,16 +343,18 @@ private static List<String> tokenize(String headerValue) {
330343
do {
331344
int nextIndex = index + 1;
332345
boolean quoted = false;
346+
boolean escaped = false;
333347
while (nextIndex < headerValue.length()) {
334348
char ch = headerValue.charAt(nextIndex);
335349
if (ch == ';') {
336350
if (!quoted) {
337351
break;
338352
}
339353
}
340-
else if (ch == '"') {
354+
else if (!escaped && ch == '"') {
341355
quoted = !quoted;
342356
}
357+
escaped = (!escaped && ch == '\\');
343358
nextIndex++;
344359
}
345360
String part = headerValue.substring(index + 1, nextIndex).trim();
@@ -356,22 +371,15 @@ else if (ch == '"') {
356371
/**
357372
* Decode the given header field param as describe in RFC 5987.
358373
* <p>Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported.
359-
* @param input the header field param
374+
* @param filename the header field param
375+
* @param charset the charset to use
360376
* @return the encoded header field param
361377
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
362378
*/
363-
private static String decodeHeaderFieldParam(String input) {
364-
Assert.notNull(input, "Input String should not be null");
365-
int firstQuoteIndex = input.indexOf('\'');
366-
int secondQuoteIndex = input.indexOf('\'', firstQuoteIndex + 1);
367-
// US_ASCII
368-
if (firstQuoteIndex == -1 || secondQuoteIndex == -1) {
369-
return input;
370-
}
371-
Charset charset = Charset.forName(input.substring(0, firstQuoteIndex));
372-
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
373-
"Charset should be UTF-8 or ISO-8859-1");
374-
byte[] value = input.substring(secondQuoteIndex + 1, input.length()).getBytes(charset);
379+
private static String decodeFilename(String filename, Charset charset) {
380+
Assert.notNull(filename, "'input' String` should not be null");
381+
Assert.notNull(charset, "'charset' should not be null");
382+
byte[] value = filename.getBytes(charset);
375383
ByteArrayOutputStream bos = new ByteArrayOutputStream();
376384
int index = 0;
377385
while (index < value.length) {
@@ -380,13 +388,18 @@ private static String decodeHeaderFieldParam(String input) {
380388
bos.write((char) b);
381389
index++;
382390
}
383-
else if (b == '%') {
384-
char[] array = { (char)value[index + 1], (char)value[index + 2]};
385-
bos.write(Integer.parseInt(String.valueOf(array), 16));
391+
else if (b == '%' && index < value.length - 2) {
392+
char[] array = new char[]{(char) value[index + 1], (char) value[index + 2]};
393+
try {
394+
bos.write(Integer.parseInt(String.valueOf(array), 16));
395+
}
396+
catch (NumberFormatException ex) {
397+
throw new IllegalArgumentException(INVALID_HEADER_FIELD_PARAMETER_FORMAT, ex);
398+
}
386399
index+=3;
387400
}
388401
else {
389-
throw new IllegalArgumentException("Invalid header field parameter format (as defined in RFC 5987)");
402+
throw new IllegalArgumentException(INVALID_HEADER_FIELD_PARAMETER_FORMAT);
390403
}
391404
}
392405
return new String(bos.toByteArray(), charset);
@@ -398,6 +411,23 @@ private static boolean isRFC5987AttrChar(byte c) {
398411
c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';
399412
}
400413

414+
private static String escapeQuotationsInFilename(String filename) {
415+
if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) {
416+
return filename;
417+
}
418+
boolean escaped = false;
419+
StringBuilder sb = new StringBuilder();
420+
for (char c : filename.toCharArray()) {
421+
sb.append((c == '"' && !escaped) ? "\\\"" : c);
422+
escaped = (!escaped && c == '\\');
423+
}
424+
// Remove backslash at the end..
425+
if (escaped) {
426+
sb.deleteCharAt(sb.length() - 1);
427+
}
428+
return sb.toString();
429+
}
430+
401431
/**
402432
* Encode the given header field param as describe in RFC 5987.
403433
* @param input the header field param
@@ -406,14 +436,11 @@ private static boolean isRFC5987AttrChar(byte c) {
406436
* @return the encoded header field param
407437
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
408438
*/
409-
private static String encodeHeaderFieldParam(String input, Charset charset) {
410-
Assert.notNull(input, "Input String should not be null");
411-
Assert.notNull(charset, "Charset should not be null");
412-
if (StandardCharsets.US_ASCII.equals(charset)) {
413-
return input;
414-
}
415-
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
416-
"Charset should be UTF-8 or ISO-8859-1");
439+
private static String encodeFilename(String input, Charset charset) {
440+
Assert.notNull(input, "`input` is required");
441+
Assert.notNull(charset, "`charset` is required");
442+
Assert.isTrue(!StandardCharsets.US_ASCII.equals(charset), "ASCII does not require encoding");
443+
Assert.isTrue(UTF_8.equals(charset) || ISO_8859_1.equals(charset), "Only UTF-8 and ISO-8859-1 supported.");
417444
byte[] source = input.getBytes(charset);
418445
int len = source.length;
419446
StringBuilder sb = new StringBuilder(len << 1);
@@ -446,7 +473,11 @@ public interface Builder {
446473
Builder name(String name);
447474

448475
/**
449-
* Set the value of the {@literal filename} parameter.
476+
* Set the value of the {@literal filename} parameter. The given
477+
* filename will be formatted as quoted-string, as defined in RFC 2616,
478+
* section 2.2, and any quote characters within the filename value will
479+
* be escaped with a backslash, e.g. {@code "foo\"bar.txt"} becomes
480+
* {@code "foo\\\"bar.txt"}.
450481
*/
451482
Builder filename(String filename);
452483

@@ -527,12 +558,14 @@ public Builder name(String name) {
527558

528559
@Override
529560
public Builder filename(String filename) {
561+
Assert.hasText(filename, "No filename");
530562
this.filename = filename;
531563
return this;
532564
}
533565

534566
@Override
535567
public Builder filename(String filename, Charset charset) {
568+
Assert.hasText(filename, "No filename");
536569
this.filename = filename;
537570
this.charset = charset;
538571
return this;

0 commit comments

Comments
 (0)