1
0
mirror of synced 2026-05-22 21:33:16 +00:00

Decode percent-encoded values

Closes gh-19136

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
This commit is contained in:
Josh Cummings
2026-04-29 08:57:16 -06:00
parent 6343002b32
commit b075f0df02
2 changed files with 21 additions and 2 deletions
@@ -17,6 +17,7 @@
package org.springframework.security.web; package org.springframework.security.web;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Map.Entry; 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.security.crypto.keygen.StringKeyGenerator;
import org.springframework.web.util.HtmlUtils; import org.springframework.web.util.HtmlUtils;
import org.springframework.web.util.UriComponentsBuilder; 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 * 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(); final StringBuilder hiddenInputsHtmlBuilder = new StringBuilder();
for (final Entry<String, List<String>> entry : uriComponentsBuilder.build().getQueryParams().entrySet()) { for (final Entry<String, List<String>> entry : uriComponentsBuilder.build().getQueryParams().entrySet()) {
final String name = entry.getKey(); final String name = UriUtils.decode(entry.getKey(), StandardCharsets.UTF_8);
for (final String value : entry.getValue()) { for (final String raw : entry.getValue()) {
if (raw == null) {
continue;
}
final String value = UriUtils.decode(raw, StandardCharsets.UTF_8);
// @formatter:off // @formatter:off
final String hiddenInput = HIDDEN_INPUT_TEMPLATE final String hiddenInput = HIDDEN_INPUT_TEMPLATE
.replace("{{name}}", HtmlUtils.htmlEscape(name)) .replace("{{name}}", HtmlUtils.htmlEscape(name))
@@ -101,6 +101,19 @@ public class FormPostRedirectStrategyTests {
assertThat(this.response).satisfies(hasScriptSrcNonce()); 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("<input name=\"payload\" type=\"hidden\" value=\"a+b/c=\" />");
assertThat(this.response).satisfies(hasScriptSrcNonce());
}
private ThrowingConsumer<MockHttpServletResponse> hasScriptSrcNonce() { private ThrowingConsumer<MockHttpServletResponse> hasScriptSrcNonce() {
return (response) -> { return (response) -> {
final String policyDirective = response.getHeader("Content-Security-Policy"); final String policyDirective = response.getHeader("Content-Security-Policy");