From 5a4a46af7811c00c748e5522813200cc17bb1b2e Mon Sep 17 00:00:00 2001 From: luozhenyu Date: Fri, 31 Mar 2023 14:16:37 +0800 Subject: [PATCH 1/2] Quote question marks in content-disposition This commit ensures that question marks are encoded, in accordance with RFC 2047, section 4.2, rule (3). Closes gh-30252 --- .../springframework/http/ContentDisposition.java | 2 +- .../http/ContentDispositionTests.java | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) 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 8516e898e952..aa7c03580df2 100644 --- a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java +++ b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java @@ -599,7 +599,7 @@ private static String encodeQuotedPrintableFilename(String filename, Charset cha } private static boolean isPrintable(byte c) { - return (c >= '!' && c <= '<') || (c >= '>' && c <= '~'); + return (c >= '!' && c <= '<') || (c >= '@' && c <= '~') || c == '>'; } private static String encodeQuotedPairs(String filename) { 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 ac4a83154304..e46a361a2620 100644 --- a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java +++ b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java @@ -325,4 +325,20 @@ void parseFormatted() { assertThat(parsed.toString()).isEqualTo(cd.toString()); } + @Test // gh-30252 + void parseFormattedWithQuestionMark() { + String filename = "filename with ?问号.txt"; + ContentDisposition cd = ContentDisposition.attachment() + .filename(filename, StandardCharsets.UTF_8) + .build(); + String[] parts = cd.toString().split("; "); + + String quotedPrintableFilename = parts[0] + "; " + parts[1]; + assertThat(ContentDisposition.parse(quotedPrintableFilename).getFilename()) + .isEqualTo(filename); + + String rfc5987Filename = parts[0] + "; " + parts[2]; + assertThat(ContentDisposition.parse(rfc5987Filename).getFilename()) + .isEqualTo(filename); + } } From 74d3268656d15413a62bb51fc7707d410596d9fe Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 18 Apr 2023 15:30:31 +0200 Subject: [PATCH 2/2] Polish contribution This commit polishes an external contribution, ensuring that not just spaces are encoded as underscores, and that underscores are encoded as non-printable. See gh-30252 --- .../http/ContentDisposition.java | 27 ++++++++++++++++--- .../http/ContentDispositionTests.java | 8 +++++- 2 files changed, 31 insertions(+), 4 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 aa7c03580df2..30d8eca9e853 100644 --- a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java +++ b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java @@ -23,6 +23,7 @@ import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Base64; +import java.util.BitSet; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -58,6 +59,19 @@ public final class ContentDisposition { private static final String INVALID_HEADER_FIELD_PARAMETER_FORMAT = "Invalid header field parameter format (as defined in RFC 5987)"; + private static final BitSet PRINTABLE = new BitSet(256); + + + static { + // RFC 2045, Section 6.7, and RFC 2047, Section 4.2 + for (int i=33; i<= 126; i++) { + PRINTABLE.set(i); + } + PRINTABLE.set(61, false); // = + PRINTABLE.set(63, false); // ? + PRINTABLE.set(95, false); // _ + } + @Nullable private final String type; @@ -545,7 +559,7 @@ private static String decodeQuotedPrintableFilename(String filename, Charset cha int index = 0; while (index < value.length) { byte b = value[index]; - if (b == '_') { + if (b == '_') { // RFC 2047, section 4.2, rule (2) baos.write(' '); index++; } @@ -583,7 +597,10 @@ private static String encodeQuotedPrintableFilename(String filename, Charset cha sb.append(charset.name()); sb.append("?Q?"); for (byte b : source) { - if (isPrintable(b)) { + if (b == 32) { // RFC 2047, section 4.2, rule (2) + sb.append('_'); + } + else if (isPrintable(b)) { sb.append((char) b); } else { @@ -599,7 +616,11 @@ private static String encodeQuotedPrintableFilename(String filename, Charset cha } private static boolean isPrintable(byte c) { - return (c >= '!' && c <= '<') || (c >= '@' && c <= '~') || c == '>'; + int b = c; + if (b < 0) { + b = 256 + b; + } + return PRINTABLE.get(b); } private static String encodeQuotedPairs(String filename) { 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 e46a361a2620..034fb065ab21 100644 --- a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java +++ b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java @@ -331,7 +331,12 @@ void parseFormattedWithQuestionMark() { ContentDisposition cd = ContentDisposition.attachment() .filename(filename, StandardCharsets.UTF_8) .build(); - String[] parts = cd.toString().split("; "); + String result = cd.toString(); + assertThat(result).isEqualTo("attachment; " + + "filename=\"=?UTF-8?Q?filename_with_=3F=E9=97=AE=E5=8F=B7.txt?=\"; " + + "filename*=UTF-8''filename%20with%20%3F%E9%97%AE%E5%8F%B7.txt"); + + String[] parts = result.split("; "); String quotedPrintableFilename = parts[0] + "; " + parts[1]; assertThat(ContentDisposition.parse(quotedPrintableFilename).getFilename()) @@ -341,4 +346,5 @@ void parseFormattedWithQuestionMark() { assertThat(ContentDisposition.parse(rfc5987Filename).getFilename()) .isEqualTo(filename); } + }