From 243b6ba55221c266e725ae57ebe3caf65f831dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Barto=C5=A1?= Date: Mon, 31 Jan 2022 11:56:06 +0100 Subject: [PATCH] Test scenarios for verifying of JS injection for WebAuthn Policy Closes #9544 --- .../registration/PolicyJsInjectionTest.java | 175 ++++++++++++++++++ .../PwdLessPolicyJsInjectionTest.java | 31 ++++ 2 files changed, 206 insertions(+) create mode 100644 testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/registration/PolicyJsInjectionTest.java create mode 100644 testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/registration/passwordless/PwdLessPolicyJsInjectionTest.java diff --git a/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/registration/PolicyJsInjectionTest.java b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/registration/PolicyJsInjectionTest.java new file mode 100644 index 0000000000..fb3798ae9b --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/registration/PolicyJsInjectionTest.java @@ -0,0 +1,175 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.webauthn.registration; + +import org.hamcrest.Matchers; +import org.junit.Test; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.testsuite.webauthn.AbstractWebAuthnVirtualTest; +import org.keycloak.testsuite.webauthn.pages.WebAuthnAuthenticatorsList; +import org.keycloak.testsuite.webauthn.updaters.AbstractWebAuthnRealmUpdater; +import org.keycloak.testsuite.webauthn.utils.WebAuthnRealmData; +import org.keycloak.utils.StringUtil; + +import java.io.Closeable; +import java.io.IOException; +import java.util.function.Consumer; +import java.util.function.Function; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Verify JS attack in WebAuthn Policy + * + * @author Martin Bartos + */ +public class PolicyJsInjectionTest extends AbstractWebAuthnVirtualTest { + // If there's an unexpected alert, the test fails -> we verified here that the script is not executed + protected final String PROMPT_SCRIPT = "required\"; window.prompt('Injection'); \" "; + + // The page is redirect, if the script is executed + protected final String REDIRECT_SCRIPT = "required\"; window.location.href = \"http://www.keycloak.org\";\""; + + @Test + public void relyingPartyEntityName() { + verifyInjection((updater) -> updater.setWebAuthnPolicyRpEntityName(REDIRECT_SCRIPT), + WebAuthnRealmData::getRpEntityName, + REDIRECT_SCRIPT); + } + + @Test + public void relyingPartyId() throws IOException { + try (Closeable u = getWebAuthnRealmUpdater() + .setWebAuthnPolicyRpId(PROMPT_SCRIPT) + .update()) { + + WebAuthnRealmData data = new WebAuthnRealmData(testRealm().toRepresentation(), isPasswordless()); + assertThat(data.getRpId(), is(PROMPT_SCRIPT)); + + registerDefaultUser(false); + + webAuthnErrorPage.assertCurrent(); + assertThat(webAuthnErrorPage.getError(), containsString("The relying party ID is not a registrable domain suffix of, nor equal to the current domain.")); + } + } + + @Test + public void attestationConveyancePreference() { + verifyInjection((updater) -> updater.setWebAuthnPolicyAttestationConveyancePreference(REDIRECT_SCRIPT), + WebAuthnRealmData::getAttestationConveyancePreference, + REDIRECT_SCRIPT, + "Failed to read the 'attestation' property from 'PublicKeyCredentialCreationOptions': The provided value 'required\"; window.location.href = \"http://www.keycloak.org\";\"' is not a valid enum value of type AttestationConveyancePreference."); + } + + @Test + public void authenticatorAttachment() { + verifyInjection((updater) -> updater.setWebAuthnPolicyAuthenticatorAttachment(REDIRECT_SCRIPT), + WebAuthnRealmData::getAuthenticatorAttachment, + REDIRECT_SCRIPT, + "Failed to read the 'authenticatorAttachment' property from 'AuthenticatorSelectionCriteria': The provided value 'required\"; window.location.href = \"http://www.keycloak.org\";\"' is not a valid enum value of type AuthenticatorAttachment."); + } + + @Test + public void requireResidentKey() { + // requireResidentKey is set to 'false' and the value is ignored -> success + verifyInjection((updater) -> updater.setWebAuthnPolicyRequireResidentKey(PROMPT_SCRIPT), + WebAuthnRealmData::getRequireResidentKey, + PROMPT_SCRIPT); + } + + @Test + public void userVerificationRequirement() { + verifyInjection((updater) -> updater.setWebAuthnPolicyUserVerificationRequirement(PROMPT_SCRIPT), + WebAuthnRealmData::getUserVerificationRequirement, + PROMPT_SCRIPT, + "Failed to read the 'userVerification' property from 'AuthenticatorSelectionCriteria': The provided value 'required\"; window.prompt('Injection'); \" ' is not a valid enum value of type UserVerificationRequirement."); + } + + @Test + public void injectUserLabel() { + final String originalLabel = "label'`;window.prompt(\"another\");'"; + + registerDefaultUser(originalLabel); + + appPage.assertCurrent(); + + final CredentialRepresentation credential = userResource().credentials() + .stream() + .filter(f -> f.getType().equals(getCredentialType())) + .findFirst() + .orElse(null); + + assertThat(credential, notNullValue()); + assertThat(credential.getUserLabel(), is(originalLabel)); + + if (!isPasswordless()) { + logout(); + + loginPage.open(); + loginPage.assertCurrent(TEST_REALM_NAME); + loginPage.login(USERNAME, PASSWORD); + + webAuthnLoginPage.assertCurrent(); + WebAuthnAuthenticatorsList authenticators = webAuthnLoginPage.getAuthenticators(); + assertThat(authenticators, notNullValue()); + assertThat(authenticators.getItems(), not(Matchers.empty())); + + assertThat(authenticators.getLabels().get(0), is("label`;window.prompt(\"another\");")); + } + } + + private void verifyInjection(Consumer> realmSetter, Function realmGetter, String requiredValue) { + verifyInjection(realmSetter, realmGetter, requiredValue, ""); + } + + /** + * Verify the possibility of executing the JS injection in WebAuthn Policy settings + * + * @param realmSetter set Realm WebAuthn policy + * @param realmGetter get Realm WebAuthn policy + * @param expectedValue expected value save in realm + * @param errorMessage expected message if it's present + */ + private void verifyInjection(Consumer> realmSetter, Function realmGetter, String expectedValue, String errorMessage) { + AbstractWebAuthnRealmUpdater updater = getWebAuthnRealmUpdater(); + realmSetter.accept(updater); + + try (Closeable u = updater.update()) { + + WebAuthnRealmData data = new WebAuthnRealmData(testRealm().toRepresentation(), isPasswordless()); + assertThat(realmGetter.apply(data), is(expectedValue)); + + boolean shouldSuccess = StringUtil.isBlank(errorMessage); + + registerDefaultUser(shouldSuccess); + + if (shouldSuccess) { + appPage.assertCurrent(); + } else { + webAuthnErrorPage.assertCurrent(); + assertThat(webAuthnErrorPage.getError(), containsString(errorMessage)); + } + } catch (IOException e) { + throw new RuntimeException("Cannot verify test scenarios for WebAuthn Policy JS Injection", e); + } + } +} diff --git a/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/registration/passwordless/PwdLessPolicyJsInjectionTest.java b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/registration/passwordless/PwdLessPolicyJsInjectionTest.java new file mode 100644 index 0000000000..1ff925090a --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/webauthn/src/test/java/org/keycloak/testsuite/webauthn/registration/passwordless/PwdLessPolicyJsInjectionTest.java @@ -0,0 +1,31 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.webauthn.registration.passwordless; + +import org.keycloak.testsuite.webauthn.registration.PolicyJsInjectionTest; + +/** + * @author Martin Bartos + */ +public class PwdLessPolicyJsInjectionTest extends PolicyJsInjectionTest { + + @Override + public boolean isPasswordless() { + return true; + } +}