VYPR
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.

PackageAffected versionsPatched versions
org.keycloak:keycloak-saml-coreMaven
< 18.0.018.0.0

Affected products

1

Patches

1
44000caaf505

KEYCLOAK-19177 Disable ECP flow by default for all Saml clients; ecp flow creates only transient users sessions

https://github.com/keycloak/keycloakMichal HajasOct 4, 2021via ghsa
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

News mentions

0

No linked articles in our index yet.