CVE-2021-3827 (Java, published 2022-08-23T16:15Z)
Description: value: 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.
Severity: nan (CVSS v3 base score: 6.8)
Weakness: CWE-287 \u2014 Improper Authentication
When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
Repository: https://github.com/keycloak/keycloak
Files changed:
services/src/main/java/org/keycloak/protocol/saml/SamlClient.java
services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java
services/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java
services/src/main/java/org/keycloak/protocol/saml/SamlRepresentationAttributes.java
services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SOAPBindingTest.java
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/settings/ClientSettingsForm.java
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/AbstractClientTest.java
themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
Commit message: KEYCLOAK-19177 Disable ECP flow by default for all Saml clients; ecp flow creates only transient users sessions


Vulnerable code:
import java.util.Map;
import static org.junit.Assert.assertThat;
                "saml.signing.certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw=="
import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_ONETIMEUSE_CONDITION;

Fixed code:
    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));
    }

    String SAML_ALLOW_ECP_FLOW = "saml.allow.ecp.flow";
        if (rep.getAllowEcpFlow() == null) {
            client.setAllowECPFlow(false);
        }

    public String getAllowEcpFlow() {
        if (getAttributes() == null) return null;
        return getAttributes().get(SamlConfigAttributes.SAML_ALLOW_ECP_FLOW);
    }

import org.keycloak.protocol.saml.SamlClient;
import org.keycloak.services.managers.AuthenticationManager;
                    // 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.");
                    }

        // Saml ECP flow creates only TRANSIENT user sessions
        authSession.setClientNote(AuthenticationManager.USER_SESSION_PERSISTENT_STATE, UserSessionModel.SessionPersistenceState.TRANSIENT.toString());
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
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.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.keycloak.testsuite.util.Matchers.isSamlResponse;
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;

    @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());
        });


    }
                "saml.signing.certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw==",
                "saml.allow.ecp.flow": "true"
import org.keycloak.protocol.saml.SamlConfigAttributes;
        public static final String SAML_ALLOW_ECP_FLOW = SamlConfigAttributes.SAML_ALLOW_ECP_FLOW;
import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_ALLOW_ECP_FLOW;
        attributes.put(SAML_ALLOW_ECP_FLOW, "false");
allow-ecp-flow=Allow ECP Flow
allow-ecp-flow.tooltip=This client is allowed to use ECP flow for authenticating users.
    $scope.samlAllowECPFlow = 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.samlAllowECPFlow == true) {
            $scope.clientEdit.attributes["saml.allow.ecp.flow"] = "true";
        } else {
            $scope.clientEdit.attributes["saml.allow.ecp.flow"] = "false";

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