Skip to content

Commit 2ba7a51

Browse files
rmartincabstractj
authored andcommitted
Escape action in the form_post response mode (keycloak#60)
Closes keycloak/keycloak-private#31 Closes https://issues.redhat.com/browse/RHBK-652 Signed-off-by: rmartinc <[email protected]>
1 parent ba8c22e commit 2ba7a51

File tree

6 files changed

+72
-2
lines changed

6 files changed

+72
-2
lines changed

saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ public String buildHtmlForm(String actionUrl, Map<String, String> inputTypes) {
381381
.append("</HEAD>")
382382
.append("<BODY Onload=\"document.forms[0].submit()\">")
383383

384-
.append("<FORM METHOD=\"POST\" ACTION=\"").append(actionUrl).append("\">");
384+
.append("<FORM METHOD=\"POST\" ACTION=\"").append(escapeAttribute(actionUrl)).append("\">");
385385

386386
builder.append("<p>Redirecting, please wait.</p>");
387387

services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,9 @@ public Response build() {
160160
builder.append(" </HEAD>");
161161
builder.append(" <BODY Onload=\"document.forms[0].submit()\">");
162162

163-
builder.append(" <FORM METHOD=\"POST\" ACTION=\"" + redirectUri.toString() + "\">");
163+
builder.append(" <FORM METHOD=\"POST\" ACTION=\"")
164+
.append(HtmlUtils.escapeAttribute(redirectUri.toString()))
165+
.append("\">");
164166

165167
for (Map.Entry<String, String> param : params.entrySet()) {
166168
builder.append(" <INPUT TYPE=\"HIDDEN\" NAME=\"")

testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,9 @@ public String getCurrentRequest() {
13981398
int index = driver.getCurrentUrl().indexOf('?');
13991399
if (index == -1) {
14001400
index = driver.getCurrentUrl().indexOf('#');
1401+
if (index == -1) {
1402+
index = driver.getCurrentUrl().length();
1403+
}
14011404
}
14021405
return driver.getCurrentUrl().substring(0, index);
14031406
}

testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,20 @@ public static SAMLDocumentHolder extractSamlResponseFromForm(String responsePage
639639
return SAMLRequestParser.parseResponsePostBinding(respElement.val());
640640
}
641641

642+
/**
643+
* Extracts the form element from a Post binding.
644+
*
645+
* @param responsePage HTML code in the page
646+
* @return The element that is the form
647+
*/
648+
public static Element extractFormFromPostResponse(String responsePage) {
649+
org.jsoup.nodes.Document theResponsePage = Jsoup.parse(responsePage);
650+
Elements form = theResponsePage.select("form");
651+
assertThat("Checking uniqueness of SAMLResponse/SAMLRequest form in Post binding", form.size(), is(1));
652+
653+
return form.first();
654+
}
655+
642656
/**
643657
* Extracts and parses value of RelayState input field of a form present in the given page.
644658
*

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*/
1717
package org.keycloak.testsuite.oauth;
1818

19+
import org.hamcrest.MatcherAssert;
20+
import org.hamcrest.Matchers;
1921
import org.jboss.arquillian.graphene.page.Page;
2022
import org.junit.Assert;
2123
import org.junit.Before;
@@ -25,6 +27,7 @@
2527
import org.keycloak.OAuthErrorException;
2628
import org.keycloak.events.Details;
2729
import org.keycloak.events.Errors;
30+
import org.keycloak.events.EventType;
2831
import org.keycloak.models.Constants;
2932
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
3033
import org.keycloak.representations.idm.RealmRepresentation;
@@ -35,6 +38,7 @@
3538
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
3639
import org.keycloak.testsuite.util.ClientManager;
3740
import org.keycloak.testsuite.util.OAuthClient;
41+
import org.keycloak.testsuite.util.WaitUtils;
3842
import org.openqa.selenium.By;
3943

4044
import jakarta.ws.rs.core.UriBuilder;
@@ -249,6 +253,28 @@ public void authorizationRequestFormPostResponseModeInvalidRedirectUri() throws
249253
}
250254
}
251255

256+
@Test
257+
public void authorizationRequestFormPostResponseModeHTMLEntitiesRedirectUri() throws IOException {
258+
try (var c = ClientAttributeUpdater.forClient(adminClient, "test", "test-app")
259+
.setRedirectUris(Collections.singletonList("*"))
260+
.update()) {
261+
oauth.responseMode(OIDCResponseMode.FORM_POST.value());
262+
oauth.responseType(OAuth2Constants.CODE);
263+
oauth.redirectUri("/test?p=&gt;"); // set HTML entity &gt;
264+
oauth.doLogin("test-user@localhost", "password");
265+
266+
WaitUtils.waitForPageToLoad();
267+
// if not properly encoded %3E would be received instead of &gt;
268+
MatcherAssert.assertThat("Redirect page was not encoded", oauth.getDriver().getCurrentUrl(), Matchers.endsWith("/test?p=&gt;"));
269+
270+
events.expect(EventType.LOGIN)
271+
.user(AssertEvents.isUUID())
272+
.session(AssertEvents.isUUID())
273+
.detail(Details.USERNAME, "test-user@localhost")
274+
.assertEvent();
275+
}
276+
}
277+
252278
@Test
253279
public void authorizationRequestFormPostResponseModeWithCustomState() throws IOException {
254280
oauth.responseMode(OIDCResponseMode.FORM_POST.value());

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import static org.hamcrest.Matchers.is;
5151
import static org.hamcrest.Matchers.nullValue;
5252
import static org.hamcrest.Matchers.notNullValue;
53+
import static org.hamcrest.Matchers.endsWith;
5354
import static org.hamcrest.MatcherAssert.assertThat;
5455
import static org.hamcrest.Matchers.matchesRegex;
5556
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT;
@@ -345,4 +346,28 @@ public void testInvalidAssertionConsumerServiceURL() throws IOException {
345346
assertThat(page, containsString("Invalid redirect uri"));
346347
}
347348
}
349+
350+
@Test
351+
public void testConsumerServiceURLHtmlEntities() throws IOException {
352+
try (var c = ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
353+
.setRedirectUris(Collections.singletonList("*"))
354+
.update()) {
355+
356+
String action = new SamlClientBuilder()
357+
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, "javascript&colon;alert('xss');", Binding.POST)
358+
.build()
359+
.login().user(bburkeUser).build()
360+
.executeAndTransform(response -> {
361+
assertThat(response, statusCodeIsHC(Response.Status.OK));
362+
String responsePage = EntityUtils.toString(response.getEntity(), "UTF-8");
363+
return SamlClient.extractFormFromPostResponse(responsePage)
364+
.attributes().asList().stream()
365+
.filter(a -> "action".equalsIgnoreCase(a.getKey()))
366+
.map(org.jsoup.nodes.Attribute::getValue)
367+
.findAny().orElse(null);
368+
});
369+
// if not encoded properly jsoup returns ":" instead of "&colon;"
370+
assertThat(action, endsWith("javascript&colon;alert('xss');"));
371+
}
372+
}
348373
}

0 commit comments

Comments
 (0)