High severityNVD Advisory· Published Aug 23, 2022· Updated Aug 3, 2024
CVE-2021-3827
CVE-2021-3827
Description
A flaw was found in keycloak, where the default ECP binding flow allows other authentication flows to be bypassed. By exploiting this behavior, an attacker can bypass the MFA authentication by sending a SOAP request with an AuthnRequest and Authorization header with the user's credentials. The highest threat from this vulnerability is to confidentiality and integrity.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.keycloak:keycloak-saml-coreMaven | < 18.0.0 | 18.0.0 |
Affected products
1Patches
144000caaf505KEYCLOAK-19177 Disable ECP flow by default for all Saml clients; ecp flow creates only transient users sessions
12 files changed · +135 −4
services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java+10 −1 modified@@ -26,6 +26,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder; +import org.keycloak.protocol.saml.SamlClient; import org.keycloak.protocol.saml.SamlConfigAttributes; import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.protocol.saml.SamlService; @@ -36,6 +37,7 @@ import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.validators.DestinationValidator; +import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.sessions.AuthenticationSessionModel; import org.w3c.dom.Document; @@ -44,7 +46,6 @@ import javax.xml.soap.SOAPHeaderElement; import java.io.IOException; import java.io.InputStream; -import java.util.Map; import java.util.Objects; /** @@ -79,6 +80,12 @@ protected boolean isDestinationRequired() { @Override protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) { + // Do not allow ECP login when client does not support it + if (!new SamlClient(client).allowECPFlow()) { + logger.errorf("Client %s is not allowed to execute ECP flow", client.getClientId()); + throw new RuntimeException("Client is not allowed to use ECP profile."); + } + // force passive authentication when executing this profile requestAbstractType.setIsPassive(true); requestAbstractType.setDestination(session.getContext().getUri().getAbsolutePath()); @@ -99,6 +106,8 @@ protected Response loginRequest(String relayState, AuthnRequestType requestAbstr @Override protected Response newBrowserAuthentication(AuthenticationSessionModel authSession, boolean isPassive, boolean redirectToAuthentication, SamlProtocol samlProtocol) { + // Saml ECP flow creates only TRANSIENT user sessions + authSession.setClientNote(AuthenticationManager.USER_SESSION_PERSISTENT_STATE, UserSessionModel.SessionPersistenceState.TRANSIENT.toString()); return super.newBrowserAuthentication(authSession, isPassive, redirectToAuthentication, createEcpSamlProtocol()); }
services/src/main/java/org/keycloak/protocol/saml/SamlClient.java+8 −0 modified@@ -120,6 +120,14 @@ public void setForceNameIDFormat(boolean val) { client.setAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, Boolean.toString(val)); } + public boolean allowECPFlow() { + return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_ALLOW_ECP_FLOW)); + } + + public void setAllowECPFlow(boolean val) { + client.setAttribute(SamlConfigAttributes.SAML_ALLOW_ECP_FLOW, Boolean.toString(val)); + } + public boolean forceArtifactBinding(){ return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING)); }
services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java+1 −0 modified@@ -44,4 +44,5 @@ public interface SamlConfigAttributes { String SAML_ENCRYPTION_PRIVATE_KEY_ATTRIBUTE = "saml.encryption." + CertificateInfoHelper.PRIVATE_KEY; String SAML_ASSERTION_LIFESPAN = "saml.assertion.lifespan"; String SAML_ARTIFACT_BINDING_IDENTIFIER = "saml.artifact.binding.identifier"; + String SAML_ALLOW_ECP_FLOW = "saml.allow.ecp.flow"; }
services/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java+4 −0 modified@@ -154,6 +154,10 @@ public void setupClientDefaults(ClientRepresentation clientRep, ClientModel newC client.setForceNameIDFormat(false); } + if (rep.getAllowEcpFlow() == null) { + client.setAllowECPFlow(false); + } + if (rep.getSamlServerSignature() == null) { client.setRequiresRealmSignature(true); }
services/src/main/java/org/keycloak/protocol/saml/SamlRepresentationAttributes.java+5 −0 modified@@ -61,6 +61,11 @@ public String getForceNameIDFormat() { return getAttributes().get(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE); } + public String getAllowEcpFlow() { + if (getAttributes() == null) return null; + return getAttributes().get(SamlConfigAttributes.SAML_ALLOW_ECP_FLOW); + } + public String getSamlArtifactBinding() { if (getAttributes() == null) return null; return getAttributes().get(SamlConfigAttributes.SAML_ARTIFACT_BINDING);
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SOAPBindingTest.java+78 −1 modified@@ -17,17 +17,34 @@ package org.keycloak.testsuite.saml; import org.junit.Test; +import org.keycloak.dom.saml.v2.SAML2Object; +import org.keycloak.dom.saml.v2.assertion.AuthnStatementType; import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.dom.saml.v2.protocol.StatusResponseType; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.saml.SamlConfigAttributes; +import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.testsuite.updaters.ClientAttributeUpdater; import org.keycloak.testsuite.util.SamlClientBuilder; +import javax.ws.rs.core.Response; +import javax.xml.soap.MessageFactory; +import javax.xml.soap.SOAPException; +import javax.xml.soap.SOAPMessage; + +import java.io.IOException; + +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; -import static org.junit.Assert.assertThat; +import static org.hamcrest.Matchers.nullValue; +import static org.keycloak.testsuite.util.Matchers.isSamlResponse; +import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; import static org.keycloak.testsuite.util.SamlClient.Binding.POST; import static org.keycloak.testsuite.util.SamlClient.Binding.SOAP; @@ -214,4 +231,64 @@ public void soapBindingLogoutWithoutSignatureMissingDestinationTest() { assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class)); } + + @Test + public void soapBindingIsNotPossibleForClientsWithSamlEcpFlowAttributeFalse() { + // Disable ECP_FLOW_ENABLED switch + getCleanup().addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_ECP_SP) + .setAttribute(SamlConfigAttributes.SAML_ALLOW_ECP_FLOW, "false") + .setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "false") + .setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false") + .update()); + + new SamlClientBuilder() + .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SAML_ASSERTION_CONSUMER_URL_ECP_SP, SOAP) + .basicAuthentication(bburkeUser) + .build() + .execute(response -> { + assertThat(response, statusCodeIsHC(Response.Status.INTERNAL_SERVER_ERROR)); + + try { + MessageFactory messageFactory = MessageFactory.newInstance(); + SOAPMessage soapMessage = messageFactory.createMessage(null, response.getEntity().getContent()); + String faultDetail = soapMessage.getSOAPBody().getFault().getDetail().getValue(); + assertThat(faultDetail, is(equalTo("Client is not allowed to use ECP profile."))); + } catch (SOAPException | IOException e) { + throw new RuntimeException(e); + } + }); + + } + + @Test + public void ecpFlowCreatesTransientSessions() { + // Disable ECP_FLOW_ENABLED switch + getCleanup().addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_ECP_SP) + .setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "false") + .setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false") + .update()); + + // Successfully login using ECP flow + SAML2Object samlObject = new SamlClientBuilder() + .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SAML_ASSERTION_CONSUMER_URL_ECP_SP, SOAP) + .basicAuthentication(bburkeUser) + .build() + .executeAndTransform(SOAP::extractResponse).getSamlObject(); + + assertThat(samlObject, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); + ResponseType loginResp1 = (ResponseType) samlObject; + AuthnStatementType sessionId = (AuthnStatementType) loginResp1.getAssertions().get(0).getAssertion().getStatements().iterator().next(); + + String userSessionId = sessionId.getSessionIndex().split("::")[0]; + + // Test that the user session with the given ID does not exist + testingClient.server().run(session -> { + RealmModel realmByName = session.realms().getRealmByName(REALM_NAME); + UserSessionModel userSession = session.sessions().getUserSession(realmByName, userSessionId); + + assertThat(userSession, nullValue()); + }); + + + } }
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json+2 −1 modified@@ -716,7 +716,8 @@ "saml.signature.algorithm": "RSA_SHA256", "saml.client.signature": "true", "saml.authnstatement": "true", - "saml.signing.certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw==" + "saml.signing.certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw==", + "saml.allow.ecp.flow": "true" } }, {
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/settings/ClientSettingsForm.java+2 −0 modified@@ -4,6 +4,7 @@ import java.util.List; import java.util.Map; +import org.keycloak.protocol.saml.SamlConfigAttributes; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.testsuite.console.page.clients.CreateClientForm; import org.keycloak.testsuite.console.page.fragment.OnOffSwitch; @@ -262,6 +263,7 @@ public class SAMLClientSettingsForm extends Form { public static final String SAML_ASSERTION_CONSUMER_URL_REDIRECT = "saml_assertion_consumer_url_redirect"; public static final String SAML_FORCE_NAME_ID_FORMAT = "saml_force_name_id_format"; public static final String SAML_NAME_ID_FORMAT = "saml_name_id_format"; + public static final String SAML_ALLOW_ECP_FLOW = SamlConfigAttributes.SAML_ALLOW_ECP_FLOW; public static final String SAML_SIGNATURE_CANONICALIZATION_METHOD = "saml_signature_canonicalization_method"; public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_POST = "saml_single_logout_service_url_post"; public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT = "saml_single_logout_service_url_redirect";
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/AbstractClientTest.java+2 −1 modified@@ -23,10 +23,10 @@ import static org.keycloak.testsuite.auth.page.login.OIDCLogin.SAML; import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_AUTHNSTATEMENT; import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_CLIENT_SIGNATURE; +import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_ALLOW_ECP_FLOW; import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_FORCE_NAME_ID_FORMAT; import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_FORCE_POST_BINDING; import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_NAME_ID_FORMAT; -import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_ONETIMEUSE_CONDITION; import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_SERVER_SIGNATURE; import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_SIGNATURE_ALGORITHM; import static org.keycloak.testsuite.util.AttributesAssert.assertEqualsBooleanAttributes; @@ -89,6 +89,7 @@ public static Map<String, String> getSAMLAttributes() { attributes.put(SAML_SIGNATURE_ALGORITHM, "RSA_SHA256"); attributes.put(SAML_FORCE_NAME_ID_FORMAT, "false"); attributes.put(SAML_NAME_ID_FORMAT, "username"); + attributes.put(SAML_ALLOW_ECP_FLOW, "false"); attributes.put(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER, ArtifactBindingUtils.computeArtifactBindingIdentifierString("saml")); return attributes; }
themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties+2 −0 modified@@ -371,6 +371,8 @@ front-channel-logout-session-required.tooltip=Specifying whether a sid (session force-name-id-format=Force Name ID Format force-name-id-format.tooltip=Ignore requested NameID subject format and use admin console configured one. +allow-ecp-flow=Allow ECP Flow +allow-ecp-flow.tooltip=This client is allowed to use ECP flow for authenticating users. name-id-format=Name ID Format name-id-format.tooltip=The name ID format to use for the subject. mapper.nameid.format.tooltip=Name ID Format using Mapper
themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js+14 −0 modified@@ -1197,6 +1197,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro $scope.samlEncrypt = false; $scope.samlForcePostBinding = false; $scope.samlForceNameIdFormat = false; + $scope.samlAllowECPFlow = false; $scope.samlXmlKeyNameTranformer = $scope.xmlKeyNameTranformers[1]; $scope.disableAuthorizationTab = !client.authorizationServicesEnabled; $scope.disableServiceAccountRolesTab = !client.serviceAccountsEnabled; @@ -1351,6 +1352,13 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro $scope.samlForceNameIdFormat = false; } } + if ($scope.client.attributes["saml.allow.ecp.flow"]) { + if ($scope.client.attributes["saml.allow.ecp.flow"] == "true") { + $scope.samlAllowECPFlow = true; + } else { + $scope.samlAllowECPFlow = false; + } + } if ($scope.client.attributes["saml.multivalued.roles"]) { if ($scope.client.attributes["saml.multivalued.roles"] == "true") { $scope.samlMultiValuedRoles = true; @@ -1961,6 +1969,12 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro } else { $scope.clientEdit.attributes["saml_force_name_id_format"] = "false"; + } + if ($scope.samlAllowECPFlow == true) { + $scope.clientEdit.attributes["saml.allow.ecp.flow"] = "true"; + } else { + $scope.clientEdit.attributes["saml.allow.ecp.flow"] = "false"; + } if ($scope.samlMultiValuedRoles == true) { $scope.clientEdit.attributes["saml.multivalued.roles"] = "true";
themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html+7 −0 modified@@ -299,6 +299,13 @@ </div> <kc-tooltip>{{:: 'force-name-id-format.tooltip' | translate}}</kc-tooltip> </div> + <div class="form-group clearfix block" data-ng-show="protocol == 'saml'"> + <label class="col-md-2 control-label" for="samlAllowECPFlow">{{:: 'allow-ecp-flow' | translate}}</label> + <div class="col-sm-6"> + <input ng-model="samlAllowECPFlow" ng-click="switchChange()" name="samlAllowECPFlow" id="samlAllowECPFlow" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/> + </div> + <kc-tooltip>{{:: 'allow-ecp-flow.tooltip' | translate}}</kc-tooltip> + </div> <div class="form-group" data-ng-show="protocol == 'saml'"> <label class="col-md-2 control-label" for="samlNameIdFormat">{{:: 'name-id-format' | translate}}</label> <div class="col-sm-6">
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-4pc7-vqv5-5r3vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-3827ghsaADVISORY
- access.redhat.com/security/cve/CVE-2021-3827ghsax_refsource_MISCWEB
- bugzilla.redhat.com/show_bug.cgighsax_refsource_MISCWEB
- github.com/keycloak/keycloak/commit/44000caaf5051d7f218d1ad79573bd3d175cad0dghsax_refsource_MISCWEB
- github.com/keycloak/keycloak/security/advisories/GHSA-4pc7-vqv5-5r3vghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.