VYPR
High severityNVD Advisory· Published Jan 16, 2020· Updated Sep 16, 2024

RFD Attack via "Content-Disposition" Header Sourced from Request Input by Spring MVC or Spring WebFlux Application

CVE-2020-5398

Description

In Spring Framework, versions 5.2.x prior to 5.2.3, versions 5.1.x prior to 5.1.13, and versions 5.0.x prior to 5.0.16, an application is vulnerable to a reflected file download (RFD) attack when it sets a "Content-Disposition" header in the response where the filename attribute is derived from user supplied input.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Spring Framework is vulnerable to reflected file download (RFD) when user input is used in Content-Disposition filename, enabling arbitrary code execution.

Vulnerability

Description

Spring Framework versions 5.2.x prior to 5.2.3, 5.1.x prior to 5.1.13, and 5.0.x prior to 5.0.16 are vulnerable to a reflected file download (RFD) attack. The vulnerability occurs when an application sets a Content-Disposition header in the response where the filename attribute is derived from user-supplied input [1]. RFD attacks leverage the browser's automatic file download behavior to trick users into executing malicious code or opening dangerous files.

Exploitation and

Attack Surface

An attacker can craft a malicious link that, when clicked by a victim, causes the Spring application to respond with a Content-Disposition header containing a crafted filename attribute. The attacker can control the filename extension, such as .bat or .js, which may be executed automatically or opened by the operating system depending on user settings. No authentication is needed if the vulnerable endpoint is publicly accessible. The attack relies on user interaction (clicking a link) and the browser's behavior of automatically downloading files based on the response headers.

Impact

Successful exploitation could allow an attacker to execute arbitrary code on the victim's machine. The impact depends on the trust the victim places in the domain and the file type used. This could lead to complete compromise of the user's system if the downloaded file is executed without proper warnings [2].

Mitigation

The Spring Framework team has fixed this issue by escaping quotes in the filename attribute of Content-Disposition headers, as shown in commit 41f40c6 [2]. Users are strongly advised to upgrade to Spring Framework 5.2.3, 5.1.13, or 5.0.16 or later. Additionally, developers should sanitize any user input used in response headers to prevent similar attacks [3].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.springframework:spring-webmvcMaven
>= 5.2.0.RELEASE, < 5.2.3.RELEASE5.2.3.RELEASE
org.springframework:spring-webmvcMaven
>= 5.1.0.RELEASE, < 5.1.13.RELEASE5.1.13.RELEASE
org.springframework:spring-webmvcMaven
>= 5.0.0.RELEASE, < 5.0.16.RELEASE5.0.16.RELEASE
org.springframework:spring-webfluxMaven
>= 5.2.0.RELEASE, < 5.2.3.RELEASE5.2.3.RELEASE
org.springframework:spring-webfluxMaven
>= 5.1.0.RELEASE, < 5.1.13.RELEASE5.1.13.RELEASE
org.springframework:spring-webfluxMaven
>= 5.0.0.RELEASE, < 5.0.16.RELEASE5.0.16.RELEASE

Affected products

10

Patches

1
41f40c6c229d

Escape quotes in filename

https://github.com/spring-projects/spring-frameworkRossen StoyanchevDec 19, 2019via ghsa
2 files changed · +80 21
  • spring-web/src/main/java/org/springframework/http/ContentDisposition.java+20 2 modified
    @@ -458,7 +458,11 @@ public interface Builder {
     		Builder name(String name);
     
     		/**
    -		 * Set the value of the {@literal filename} parameter.
    +		 * Set the value of the {@literal filename} parameter. The given
    +		 * filename will be formatted as quoted-string, as defined in RFC 2616,
    +		 * section 2.2, and any quote characters within the filename value will
    +		 * be escaped with a backslash, e.g. {@code "foo\"bar.txt"} becomes
    +		 * {@code "foo\\\"bar.txt"}.
     		 */
     		Builder filename(String filename);
     
    @@ -539,10 +543,24 @@ public Builder name(String name) {
     
     		@Override
     		public Builder filename(String filename) {
    -			this.filename = filename;
    +			Assert.hasText(filename, "No filename");
    +			this.filename = escapeQuotationMarks(filename);
     			return this;
     		}
     
    +		private static String escapeQuotationMarks(String filename) {
    +			if (filename.indexOf('"') == -1) {
    +				return filename;
    +			}
    +			boolean escaped = false;
    +			StringBuilder sb = new StringBuilder();
    +			for (char c : filename.toCharArray()) {
    +				sb.append((c == '"' && !escaped) ? "\\\"" : c);
    +				escaped = (!escaped && c == '\\');
    +			}
    +			return sb.toString();
    +		}
    +
     		@Override
     		public Builder filename(String filename, Charset charset) {
     			this.filename = filename;
    
  • spring-web/src/test/java/org/springframework/http/ContentDispositionTests.java+60 19 modified
    @@ -19,11 +19,13 @@
     import java.nio.charset.StandardCharsets;
     import java.time.ZonedDateTime;
     import java.time.format.DateTimeFormatter;
    +import java.util.function.BiConsumer;
     
     import org.junit.jupiter.api.Test;
     
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
    +import static org.springframework.http.ContentDisposition.builder;
     
     /**
      * Unit tests for {@link ContentDisposition}
    @@ -38,7 +40,7 @@ public class ContentDispositionTests {
     	@Test
     	public void parse() {
     		assertThat(parse("form-data; name=\"foo\"; filename=\"foo.txt\"; size=123"))
    -				.isEqualTo(ContentDisposition.builder("form-data")
    +				.isEqualTo(builder("form-data")
     						.name("foo")
     						.filename("foo.txt")
     						.size(123L)
    @@ -48,23 +50,23 @@ public void parse() {
     	@Test
     	public void parseFilenameUnquoted() {
     		assertThat(parse("form-data; filename=unquoted"))
    -				.isEqualTo(ContentDisposition.builder("form-data")
    +				.isEqualTo(builder("form-data")
     						.filename("unquoted")
     						.build());
     	}
     
     	@Test  // SPR-16091
     	public void parseFilenameWithSemicolon() {
     		assertThat(parse("attachment; filename=\"filename with ; semicolon.txt\""))
    -				.isEqualTo(ContentDisposition.builder("attachment")
    +				.isEqualTo(builder("attachment")
     						.filename("filename with ; semicolon.txt")
     						.build());
     	}
     
     	@Test
     	public void parseEncodedFilename() {
     		assertThat(parse("form-data; name=\"name\"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt"))
    -				.isEqualTo(ContentDisposition.builder("form-data")
    +				.isEqualTo(builder("form-data")
     						.name("name")
     						.filename("中文.txt", StandardCharsets.UTF_8)
     						.build());
    @@ -73,15 +75,15 @@ public void parseEncodedFilename() {
     	@Test // gh-24112
     	public void parseEncodedFilenameWithPaddedCharset() {
     		assertThat(parse("attachment; filename*= UTF-8''some-file.zip"))
    -				.isEqualTo(ContentDisposition.builder("attachment")
    +				.isEqualTo(builder("attachment")
     						.filename("some-file.zip", StandardCharsets.UTF_8)
     						.build());
     	}
     
     	@Test
     	public void parseEncodedFilenameWithoutCharset() {
     		assertThat(parse("form-data; name=\"name\"; filename*=test.txt"))
    -				.isEqualTo(ContentDisposition.builder("form-data")
    +				.isEqualTo(builder("form-data")
     						.name("name")
     						.filename("test.txt")
     						.build());
    @@ -104,18 +106,30 @@ public void parseEncodedFilenameWithInvalidName() {
     
     	@Test // gh-23077
     	public void parseWithEscapedQuote() {
    -		assertThat(parse("form-data; name=\"file\"; filename=\"\\\"The Twilight Zone\\\".txt\"; size=123"))
    -				.isEqualTo(ContentDisposition.builder("form-data")
    -						.name("file")
    -						.filename("\\\"The Twilight Zone\\\".txt")
    -						.size(123L)
    -						.build());
    +
    +		BiConsumer<String, String> tester = (description, filename) -> {
    +			assertThat(parse("form-data; name=\"file\"; filename=\"" + filename + "\"; size=123"))
    +					.as(description)
    +					.isEqualTo(builder("form-data").name("file").filename(filename).size(123L).build());
    +		};
    +
    +		tester.accept("Escaped quotes should be ignored",
    +				"\\\"The Twilight Zone\\\".txt");
    +
    +		tester.accept("Escaped quotes preceded by escaped backslashes should be ignored",
    +				"\\\\\\\"The Twilight Zone\\\\\\\".txt");
    +
    +		tester.accept("Escaped backslashes should not suppress quote",
    +				"The Twilight Zone \\\\");
    +
    +		tester.accept("Escaped backslashes should not suppress quote",
    +				"The Twilight Zone \\\\\\\\");
     	}
     
     	@Test
     	public void parseWithExtraSemicolons() {
     		assertThat(parse("form-data; name=\"foo\";; ; filename=\"foo.txt\"; size=123"))
    -				.isEqualTo(ContentDisposition.builder("form-data")
    +				.isEqualTo(builder("form-data")
     						.name("foo")
     						.filename("foo.txt")
     						.size(123L)
    @@ -133,7 +147,7 @@ public void parseDates() {
     						"creation-date=\"" + creationTime.format(formatter) + "\"; " +
     						"modification-date=\"" + modificationTime.format(formatter) + "\"; " +
     						"read-date=\"" + readTime.format(formatter) + "\"")).isEqualTo(
    -				ContentDisposition.builder("attachment")
    +				builder("attachment")
     						.creationDate(creationTime)
     						.modificationDate(modificationTime)
     						.readDate(readTime)
    @@ -149,7 +163,7 @@ public void parseIgnoresInvalidDates() {
     						"creation-date=\"-1\"; " +
     						"modification-date=\"-1\"; " +
     						"read-date=\"" + readTime.format(formatter) + "\"")).isEqualTo(
    -				ContentDisposition.builder("attachment")
    +				builder("attachment")
     						.readDate(readTime)
     						.build());
     	}
    @@ -177,7 +191,7 @@ private static ContentDisposition parse(String input) {
     	@Test
     	public void format() {
     		assertThat(
    -				ContentDisposition.builder("form-data")
    +				builder("form-data")
     						.name("foo")
     						.filename("foo.txt")
     						.size(123L)
    @@ -188,7 +202,7 @@ public void format() {
     	@Test
     	public void formatWithEncodedFilename() {
     		assertThat(
    -				ContentDisposition.builder("form-data")
    +				builder("form-data")
     						.name("name")
     						.filename("中文.txt", StandardCharsets.UTF_8)
     						.build().toString())
    @@ -198,18 +212,45 @@ public void formatWithEncodedFilename() {
     	@Test
     	public void formatWithEncodedFilenameUsingUsAscii() {
     		assertThat(
    -				ContentDisposition.builder("form-data")
    +				builder("form-data")
     						.name("name")
     						.filename("test.txt", StandardCharsets.US_ASCII)
     						.build()
     						.toString())
     				.isEqualTo("form-data; name=\"name\"; filename=\"test.txt\"");
     	}
     
    +	@Test // gh-24220
    +	public void formatWithFilenameWithQuotes() {
    +
    +		BiConsumer<String, String> tester = (input, output) -> {
    +			assertThat(builder("form-data").filename(input).build().toString())
    +					.isEqualTo("form-data; filename=\"" + output + "\"");
    +		};
    +
    +		String filename = "\"foo.txt";
    +		tester.accept(filename, "\\" + filename);
    +
    +		filename = "\\\"foo.txt";
    +		tester.accept(filename, filename);
    +
    +		filename = "\\\\\"foo.txt";
    +		tester.accept(filename, "\\" + filename);
    +
    +		filename = "\\\\\\\"foo.txt";
    +		tester.accept(filename, filename);
    +
    +		filename = "\\\\\\\\\"foo.txt";
    +		tester.accept(filename, "\\" + filename);
    +
    +		tester.accept("\"\"foo.txt", "\\\"\\\"foo.txt");
    +		tester.accept("\"\"\"foo.txt", "\\\"\\\"\\\"foo.txt");
    +	}
    +
     	@Test
     	public void formatWithEncodedFilenameUsingInvalidCharset() {
     		assertThatIllegalArgumentException().isThrownBy(() ->
    -				ContentDisposition.builder("form-data")
    +				builder("form-data")
     						.name("name")
     						.filename("test.txt", StandardCharsets.UTF_16)
     						.build()
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

82

News mentions

0

No linked articles in our index yet.