From b075f0df022c9ffe2d85e4bf7afbc35db0aae84d Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:57:16 -0600 Subject: [PATCH] Decode percent-encoded values Closes gh-19136 Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com> --- .../security/web/FormPostRedirectStrategy.java | 10 ++++++++-- .../security/web/FormPostRedirectStrategyTests.java | 13 +++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/FormPostRedirectStrategy.java b/web/src/main/java/org/springframework/security/web/FormPostRedirectStrategy.java index e6a2aff459..af308e72c8 100644 --- a/web/src/main/java/org/springframework/security/web/FormPostRedirectStrategy.java +++ b/web/src/main/java/org/springframework/security/web/FormPostRedirectStrategy.java @@ -17,6 +17,7 @@ package org.springframework.security.web; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; import java.util.Map.Entry; @@ -30,6 +31,7 @@ import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; import org.springframework.security.crypto.keygen.StringKeyGenerator; import org.springframework.web.util.HtmlUtils; import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; /** * Redirect using an auto-submitting HTML form using the POST method. All query params @@ -83,8 +85,12 @@ public final class FormPostRedirectStrategy implements RedirectStrategy { final StringBuilder hiddenInputsHtmlBuilder = new StringBuilder(); for (final Entry> entry : uriComponentsBuilder.build().getQueryParams().entrySet()) { - final String name = entry.getKey(); - for (final String value : entry.getValue()) { + final String name = UriUtils.decode(entry.getKey(), StandardCharsets.UTF_8); + for (final String raw : entry.getValue()) { + if (raw == null) { + continue; + } + final String value = UriUtils.decode(raw, StandardCharsets.UTF_8); // @formatter:off final String hiddenInput = HIDDEN_INPUT_TEMPLATE .replace("{{name}}", HtmlUtils.htmlEscape(name)) diff --git a/web/src/test/java/org/springframework/security/web/FormPostRedirectStrategyTests.java b/web/src/test/java/org/springframework/security/web/FormPostRedirectStrategyTests.java index f54a1e51d8..0c23d98a49 100644 --- a/web/src/test/java/org/springframework/security/web/FormPostRedirectStrategyTests.java +++ b/web/src/test/java/org/springframework/security/web/FormPostRedirectStrategyTests.java @@ -101,6 +101,19 @@ public class FormPostRedirectStrategyTests { assertThat(this.response).satisfies(hasScriptSrcNonce()); } + // gh-19136 + + @Test + public void absoluteUrlWithPercentEncodedQueryParamsRedirect() throws IOException { + this.redirectStrategy.sendRedirect(this.request, this.response, "https://example.com/cb?payload=a%2Bb%2Fc%3D"); + assertThat(this.response.getStatus()).isEqualTo(HttpStatus.OK.value()); + assertThat(this.response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE); + assertThat(this.response.getContentAsString()).contains("action=\"https://example.com/cb\""); + assertThat(this.response.getContentAsString()) + .contains(""); + assertThat(this.response).satisfies(hasScriptSrcNonce()); + } + private ThrowingConsumer hasScriptSrcNonce() { return (response) -> { final String policyDirective = response.getHeader("Content-Security-Policy");