Escape action in the form_post response mode (#60)

Closes keycloak/keycloak-private#31
Closes https://issues.redhat.com/browse/RHBK-652

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
Ricardo Martin 2023-12-06 13:53:01 +01:00 committed by Bruno Oliveira da Silva
parent ba8c22eaf0
commit 2ba7a51da6
6 changed files with 72 additions and 2 deletions

View file

@ -381,7 +381,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
.append("</HEAD>") .append("</HEAD>")
.append("<BODY Onload=\"document.forms[0].submit()\">") .append("<BODY Onload=\"document.forms[0].submit()\">")
.append("<FORM METHOD=\"POST\" ACTION=\"").append(actionUrl).append("\">"); .append("<FORM METHOD=\"POST\" ACTION=\"").append(escapeAttribute(actionUrl)).append("\">");
builder.append("<p>Redirecting, please wait.</p>"); builder.append("<p>Redirecting, please wait.</p>");

View file

@ -160,7 +160,9 @@ public abstract class OIDCRedirectUriBuilder {
builder.append(" </HEAD>"); builder.append(" </HEAD>");
builder.append(" <BODY Onload=\"document.forms[0].submit()\">"); builder.append(" <BODY Onload=\"document.forms[0].submit()\">");
builder.append(" <FORM METHOD=\"POST\" ACTION=\"" + redirectUri.toString() + "\">"); builder.append(" <FORM METHOD=\"POST\" ACTION=\"")
.append(HtmlUtils.escapeAttribute(redirectUri.toString()))
.append("\">");
for (Map.Entry<String, String> param : params.entrySet()) { for (Map.Entry<String, String> param : params.entrySet()) {
builder.append(" <INPUT TYPE=\"HIDDEN\" NAME=\"") builder.append(" <INPUT TYPE=\"HIDDEN\" NAME=\"")

View file

@ -1398,6 +1398,9 @@ public class OAuthClient {
int index = driver.getCurrentUrl().indexOf('?'); int index = driver.getCurrentUrl().indexOf('?');
if (index == -1) { if (index == -1) {
index = driver.getCurrentUrl().indexOf('#'); index = driver.getCurrentUrl().indexOf('#');
if (index == -1) {
index = driver.getCurrentUrl().length();
}
} }
return driver.getCurrentUrl().substring(0, index); return driver.getCurrentUrl().substring(0, index);
} }

View file

@ -639,6 +639,20 @@ public class SamlClient {
return SAMLRequestParser.parseResponsePostBinding(respElement.val()); return SAMLRequestParser.parseResponsePostBinding(respElement.val());
} }
/**
* Extracts the form element from a Post binding.
*
* @param responsePage HTML code in the page
* @return The element that is the form
*/
public static Element extractFormFromPostResponse(String responsePage) {
org.jsoup.nodes.Document theResponsePage = Jsoup.parse(responsePage);
Elements form = theResponsePage.select("form");
assertThat("Checking uniqueness of SAMLResponse/SAMLRequest form in Post binding", form.size(), is(1));
return form.first();
}
/** /**
* Extracts and parses value of RelayState input field of a form present in the given page. * Extracts and parses value of RelayState input field of a form present in the given page.
* *

View file

@ -16,6 +16,8 @@
*/ */
package org.keycloak.testsuite.oauth; package org.keycloak.testsuite.oauth;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
@ -25,6 +27,7 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode; import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
@ -35,6 +38,7 @@ import org.keycloak.testsuite.pages.InstalledAppRedirectPage;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater; import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.By; import org.openqa.selenium.By;
import jakarta.ws.rs.core.UriBuilder; import jakarta.ws.rs.core.UriBuilder;
@ -249,6 +253,28 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
} }
} }
@Test
public void authorizationRequestFormPostResponseModeHTMLEntitiesRedirectUri() throws IOException {
try (var c = ClientAttributeUpdater.forClient(adminClient, "test", "test-app")
.setRedirectUris(Collections.singletonList("*"))
.update()) {
oauth.responseMode(OIDCResponseMode.FORM_POST.value());
oauth.responseType(OAuth2Constants.CODE);
oauth.redirectUri("/test?p=&gt;"); // set HTML entity &gt;
oauth.doLogin("test-user@localhost", "password");
WaitUtils.waitForPageToLoad();
// if not properly encoded %3E would be received instead of &gt;
MatcherAssert.assertThat("Redirect page was not encoded", oauth.getDriver().getCurrentUrl(), Matchers.endsWith("/test?p=&gt;"));
events.expect(EventType.LOGIN)
.user(AssertEvents.isUUID())
.session(AssertEvents.isUUID())
.detail(Details.USERNAME, "test-user@localhost")
.assertEvent();
}
}
@Test @Test
public void authorizationRequestFormPostResponseModeWithCustomState() throws IOException { public void authorizationRequestFormPostResponseModeWithCustomState() throws IOException {
oauth.responseMode(OIDCResponseMode.FORM_POST.value()); oauth.responseMode(OIDCResponseMode.FORM_POST.value());

View file

@ -50,6 +50,7 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.matchesRegex; import static org.hamcrest.Matchers.matchesRegex;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT; import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT;
@ -345,4 +346,28 @@ public class BasicSamlTest extends AbstractSamlTest {
assertThat(page, containsString("Invalid redirect uri")); assertThat(page, containsString("Invalid redirect uri"));
} }
} }
@Test
public void testConsumerServiceURLHtmlEntities() throws IOException {
try (var c = ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
.setRedirectUris(Collections.singletonList("*"))
.update()) {
String action = new SamlClientBuilder()
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, "javascript&colon;alert('xss');", Binding.POST)
.build()
.login().user(bburkeUser).build()
.executeAndTransform(response -> {
assertThat(response, statusCodeIsHC(Response.Status.OK));
String responsePage = EntityUtils.toString(response.getEntity(), "UTF-8");
return SamlClient.extractFormFromPostResponse(responsePage)
.attributes().asList().stream()
.filter(a -> "action".equalsIgnoreCase(a.getKey()))
.map(org.jsoup.nodes.Attribute::getValue)
.findAny().orElse(null);
});
// if not encoded properly jsoup returns ":" instead of "&colon;"
assertThat(action, endsWith("javascript&colon;alert('xss');"));
}
}
} }