From 062032373efe9beff0a1047320d4586daccad89c Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 20 Jul 2026 14:27:03 +0200 Subject: [PATCH] Ensure parsing/tostring symmetry in ContentDisposition Prior to this commit, building a "Content-Disposition" header to a String and then parsing it back would not always result in the original header. This commit ensures that ContentDisposition guarantees this and honors the "equals" contract. Fixes gh-37064 --- .../http/ContentDisposition.java | 35 ++++++++++--------- .../http/ContentDispositionTests.java | 9 +++++ 2 files changed, 27 insertions(+), 17 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 6e2e5f2eb1d..ababdaf4dc2 100644 --- a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java +++ b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java @@ -172,17 +172,17 @@ public final class ContentDisposition { sb.append(this.type); } if (this.name != null) { - sb.append("; name=\"").append(this.name).append('\"'); + sb.append("; name=\""); + appendName(sb, this.name).append('\"'); } if (this.filename != null) { if (this.charset == null || StandardCharsets.US_ASCII.equals(this.charset)) { - sb.append("; filename=\"") - .append(encodeQuotedPairs(this.filename)) - .append('\"'); + sb.append("; filename=\""); + appendName(sb, this.filename).append('\"'); } else { sb.append("; filename=\"") - .append(transliterateToAscii(encodeQuotedPairs(this.filename))) + .append(transliterateToAscii(appendName(new StringBuilder(), this.filename).toString())) .append("\"; filename*=") .append(encodeRfc5987Filename(this.filename, this.charset)); } @@ -253,7 +253,7 @@ public final class ContentDisposition { part.substring(eqIndex + 2, part.length() - 1) : part.substring(eqIndex + 1)); if (attribute.equals("name") ) { - name = value; + name = (value.indexOf('\\') != -1 ? decodeQuotedPairs(value) : value); } else if (attribute.equals("filename*") ) { int idx1 = value.indexOf('\''); @@ -503,19 +503,20 @@ public final class ContentDisposition { return sb.toString(); } - private static String encodeQuotedPairs(String filename) { - if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) { - return filename; - } - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < filename.length() ; i++) { - char c = filename.charAt(i); - if (c == '"' || c == '\\') { - sb.append('\\'); + private static StringBuilder appendName(StringBuilder buffer, String name) { + for (int i = 0; i < name.length() ; i++) { + char c = name.charAt(i); + // strip control characters + if (c <= 0x1F || c == 0x7F) { + continue; } - sb.append(c); + // encode quoted pairs + if (c == '"' || c == '\\') { + buffer.append('\\'); + } + buffer.append(c); } - return sb.toString(); + return buffer; } private static String decodeQuotedPairs(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 2928b836763..6b3f40ea11a 100644 --- a/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java +++ b/spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java @@ -181,6 +181,15 @@ class ContentDispositionTests { assertThat(cd.getFilename()).isEqualTo("foo\\bar \"baz\" qux \\\" quux.txt"); } + @Test + void parseBackslashInName() { + String s = "form-data; name=\"foo\\\"bar\"; filename=\"foo.txt\""; + ContentDisposition cd = ContentDisposition.parse(s); + assertThat(cd.getName()).isEqualTo("foo\"bar"); + assertThat(cd.getFilename()).isEqualTo("foo.txt"); + assertThat(cd.toString()).isEqualTo(s); + } + @Test void parseBackslashInLastPosition() { ContentDisposition cd = ContentDisposition.parse("form-data; name=\"foo\"; filename=\"bar\\\"");