High severityNVD Advisory· Published Aug 5, 2022· Updated Aug 3, 2024
CVE-2022-2668
CVE-2022-2668
Description
An issue was discovered in Keycloak that allows arbitrary Javascript to be uploaded for the SAML protocol mapper even if the UPLOAD_SCRIPTS feature is disabled
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.keycloak:keycloak-parentMaven | < 19.0.2 | 19.0.2 |
Affected products
1Patches
1e2ae7eef39b2SAML javascript protocol mapper: disable uploading scripts through admin console by default (#14296)
9 files changed · +269 −6
core/src/main/java/org/keycloak/representations/provider/ScriptProviderDescriptor.java+11 −0 modified@@ -31,6 +31,8 @@ public class ScriptProviderDescriptor { public static final String POLICIES = "policies"; public static final String MAPPERS = "mappers"; + public static final String SAML_MAPPERS = "saml-mappers"; + private Map<String, List<ScriptProviderMetadata>> providers = new HashMap<>(); @JsonUnwrapped @@ -54,6 +56,11 @@ public void setMappers(List<ScriptProviderMetadata> metadata) { providers.put(MAPPERS, metadata); } + @JsonSetter(SAML_MAPPERS) + public void setSAMLMappers(List<ScriptProviderMetadata> metadata) { + providers.put(SAML_MAPPERS, metadata); + } + public void addAuthenticator(String name, String fileName) { addProvider(AUTHENTICATORS, name, fileName, null); } @@ -76,4 +83,8 @@ public void addPolicy(String name, String fileName) { public void addMapper(String name, String fileName) { addProvider(MAPPERS, name, fileName, null); } + + public void addSAMLMapper(String name, String fileName) { + addProvider(SAML_MAPPERS, name, fileName, null); + } }
quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java+8 −1 modified@@ -29,6 +29,7 @@ import static org.keycloak.representations.provider.ScriptProviderDescriptor.MAPPERS; import static org.keycloak.representations.provider.ScriptProviderDescriptor.POLICIES; import static org.keycloak.quarkus.runtime.Environment.getProviderFiles; +import static org.keycloak.representations.provider.ScriptProviderDescriptor.SAML_MAPPERS; import javax.persistence.Entity; import javax.persistence.spi.PersistenceUnitTransactionType; @@ -91,6 +92,7 @@ import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.connections.jpa.JpaConnectionSpi; import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory; +import org.keycloak.protocol.saml.mappers.DeployedScriptSAMLProtocolMapper; import org.keycloak.quarkus.runtime.QuarkusProfile; import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource; import org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource; @@ -170,6 +172,7 @@ class KeycloakProcessor { DEPLOYEABLE_SCRIPT_PROVIDERS.put(AUTHENTICATORS, KeycloakProcessor::registerScriptAuthenticator); DEPLOYEABLE_SCRIPT_PROVIDERS.put(POLICIES, KeycloakProcessor::registerScriptPolicy); DEPLOYEABLE_SCRIPT_PROVIDERS.put(MAPPERS, KeycloakProcessor::registerScriptMapper); + DEPLOYEABLE_SCRIPT_PROVIDERS.put(SAML_MAPPERS, KeycloakProcessor::registerSAMLScriptMapper); } private static ProviderFactory registerScriptAuthenticator(ScriptProviderMetadata metadata) { @@ -184,6 +187,10 @@ private static ProviderFactory registerScriptMapper(ScriptProviderMetadata metad return new DeployedScriptOIDCProtocolMapper(metadata); } + private static ProviderFactory registerSAMLScriptMapper(ScriptProviderMetadata metadata) { + return new DeployedScriptSAMLProtocolMapper(metadata); + } + @BuildStep FeatureBuildItem getFeature() { return new FeatureBuildItem("keycloak"); @@ -660,7 +667,7 @@ private ProviderFactory createDeployableScriptProvider(JarFile jarFile, Entry<St } private boolean isScriptForSpi(Spi spi, String type) { - if (spi instanceof ProtocolMapperSpi && MAPPERS.equals(type)) { + if (spi instanceof ProtocolMapperSpi && (MAPPERS.equals(type) || SAML_MAPPERS.equals(type))) { return true; } else if (spi instanceof PolicySpi && POLICIES.equals(type)) { return true;
services/src/main/java/org/keycloak/protocol/saml/mappers/DeployedScriptSAMLProtocolMapper.java+59 −0 added@@ -0,0 +1,59 @@ +package org.keycloak.protocol.saml.mappers; + +import java.util.List; +import java.util.stream.Collectors; + +import org.keycloak.common.Profile; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.provider.ScriptProviderMetadata; + +/** + * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> + */ +public class DeployedScriptSAMLProtocolMapper extends ScriptBasedMapper { + + protected ScriptProviderMetadata metadata; + + public DeployedScriptSAMLProtocolMapper(ScriptProviderMetadata metadata) { + this.metadata = metadata; + } + + public DeployedScriptSAMLProtocolMapper() { + // for reflection + } + + @Override + public String getId() { + return metadata.getId(); + } + + @Override + public String getDisplayType() { + return metadata.getName(); + } + + @Override + public String getHelpText() { + return metadata.getDescription(); + } + + @Override + protected String getScriptCode(ProtocolMapperModel mapperModel) { + return metadata.getCode(); + } + + public List<ProviderConfigProperty> getConfigProperties() { + return super.getConfigProperties().stream() + .filter(providerConfigProperty -> !ProviderConfigProperty.SCRIPT_TYPE.equals(providerConfigProperty.getName())) // filter "script" property + .collect(Collectors.toList()); + } + + public void setMetadata(ScriptProviderMetadata metadata) { + this.metadata = metadata; + } + + public ScriptProviderMetadata getMetadata() { + return metadata; + } +}
services/src/main/java/org/keycloak/protocol/saml/mappers/ScriptBasedMapper.java+14 −3 modified@@ -1,10 +1,12 @@ package org.keycloak.protocol.saml.mappers; import org.jboss.logging.Logger; +import org.keycloak.common.Profile; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; import org.keycloak.dom.saml.v2.assertion.AttributeType; import org.keycloak.models.*; import org.keycloak.protocol.ProtocolMapperConfigException; +import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.scripting.EvaluatableScriptAdapter; import org.keycloak.scripting.ScriptCompilationException; @@ -20,7 +22,7 @@ * * @author Alistair Doswald */ -public class ScriptBasedMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper { +public class ScriptBasedMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper, EnvironmentDependentProviderFactory { private static final List<ProviderConfigProperty> configProperties = new ArrayList<>(); public static final String PROVIDER_ID = "saml-javascript-mapper"; @@ -92,6 +94,11 @@ public String getHelpText() { return "Evaluates a JavaScript function to produce an attribute value based on context information."; } + @Override + public boolean isSupported() { + return Profile.isFeatureEnabled(Profile.Feature.SCRIPTS); + } + /** * This method attaches one or many attributes to the passed attribute statement. * To obtain the attribute values, it executes the mapper's script and returns attaches the returned value to the @@ -110,7 +117,7 @@ public void transformAttributeStatement(AttributeStatementType attributeStatemen KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { UserModel user = userSession.getUser(); - String scriptSource = mappingModel.getConfig().get(ProviderConfigProperty.SCRIPT_TYPE); + String scriptSource = getScriptCode(mappingModel); RealmModel realm = userSession.getRealm(); String single = mappingModel.getConfig().get(SINGLE_VALUE_ATTRIBUTE); @@ -158,7 +165,7 @@ public void transformAttributeStatement(AttributeStatementType attributeStatemen @Override public void validateConfig(KeycloakSession session, RealmModel realm, ProtocolMapperContainerModel client, ProtocolMapperModel mapperModel) throws ProtocolMapperConfigException { - String scriptCode = mapperModel.getConfig().get(ProviderConfigProperty.SCRIPT_TYPE); + String scriptCode = getScriptCode(mapperModel); if (scriptCode == null) { return; } @@ -173,6 +180,10 @@ public void validateConfig(KeycloakSession session, RealmModel realm, ProtocolMa } } + protected String getScriptCode(ProtocolMapperModel mappingModel) { + return mappingModel.getConfig().get(ProviderConfigProperty.SCRIPT_TYPE); + } + /** * Creates an protocol mapper model for the this script based mapper. This mapper model is meant to be used for * testing, as normally such objects are created in a different manner through the keycloak GUI.
services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper+0 −1 modified@@ -36,7 +36,6 @@ org.keycloak.protocol.saml.mappers.UserAttributeStatementMapper org.keycloak.protocol.saml.mappers.UserPropertyAttributeStatementMapper org.keycloak.protocol.saml.mappers.UserSessionNoteStatementMapper org.keycloak.protocol.saml.mappers.GroupMembershipMapper -org.keycloak.protocol.saml.mappers.ScriptBasedMapper org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper org.keycloak.protocol.oidc.mappers.UserRealmRoleMappingMapper org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TestCleanup.java+1 −1 modified@@ -52,7 +52,7 @@ public class TestCleanup { private final String realmName; private final ConcurrentLinkedDeque<Runnable> genericCleanups = new ConcurrentLinkedDeque<>(); - // Key is kind of entity (eg. "client", "role", "user" etc), Values are all kind of entities of given type to cleanup + // Key is kind of entity (eg. "client", "role", "user" etc), Values are all IDs of entities of given type to cleanup private final ConcurrentMultivaluedHashMap<String, String> entities = new ConcurrentMultivaluedHashMap<>();
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedSAMLScriptMapperTest.java+165 −0 added@@ -0,0 +1,165 @@ +package org.keycloak.testsuite.script; + +import java.io.IOException; +import java.util.Collections; +import java.util.stream.Stream; + +import javax.ws.rs.core.Response; + +import org.jboss.arquillian.container.test.api.Deployer; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.TargetsContainer; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.keycloak.common.Profile; +import org.keycloak.dom.saml.v2.assertion.AssertionType; +import org.keycloak.dom.saml.v2.assertion.AttributeType; +import org.keycloak.protocol.saml.SamlProtocol; +import org.keycloak.protocol.saml.mappers.AttributeStatementHelper; +import org.keycloak.protocol.saml.mappers.ScriptBasedMapper; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.keycloak.representations.provider.ScriptProviderDescriptor; +import org.keycloak.saml.common.constants.JBossSAMLURIConstants; +import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.arquillian.annotation.EnableFeatures; +import org.keycloak.testsuite.saml.AbstractSamlTest; +import org.keycloak.testsuite.saml.RoleMapperTest; +import org.keycloak.testsuite.updaters.ClientAttributeUpdater; +import org.keycloak.testsuite.updaters.ProtocolMappersUpdater; +import org.keycloak.testsuite.util.ContainerAssume; +import org.keycloak.testsuite.util.Matchers; +import org.keycloak.testsuite.util.SamlClient; +import org.keycloak.testsuite.util.SamlClientBuilder; +import org.keycloak.util.JsonSerialization; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.keycloak.common.Profile.Feature.SCRIPTS; +import static org.keycloak.testsuite.arquillian.DeploymentTargetModifier.AUTH_SERVER_CURRENT; +import static org.keycloak.testsuite.saml.RoleMapperTest.createSamlProtocolMapper; +import static org.keycloak.testsuite.util.SamlStreams.assertionsUnencrypted; +import static org.keycloak.testsuite.util.SamlStreams.attributeStatements; +import static org.keycloak.testsuite.util.SamlStreams.attributesUnecrypted; + +/** + * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> + */ +public class DeployedSAMLScriptMapperTest extends AbstractSamlTest { + + private static final String SCRIPT_DEPLOYMENT_NAME = "scripts.jar"; + + private ClientAttributeUpdater cau; + private ProtocolMappersUpdater pmu; + + @Deployment(name = SCRIPT_DEPLOYMENT_NAME, managed = false, testable = false) + @TargetsContainer(AUTH_SERVER_CURRENT) + public static JavaArchive deploy() throws IOException { + ScriptProviderDescriptor representation = new ScriptProviderDescriptor(); + + representation.addSAMLMapper("My Mapper", "mapper-a.js"); + + return ShrinkWrap.create(JavaArchive.class, SCRIPT_DEPLOYMENT_NAME) + .addAsManifestResource(new StringAsset(JsonSerialization.writeValueAsPrettyString(representation)), + "keycloak-scripts.json") + .addAsResource("scripts/mapper-example.js", "mapper-a.js"); + } + + @BeforeClass + public static void verifyEnvironment() { + ContainerAssume.assumeNotAuthServerUndertow(); + } + + @ArquillianResource + private Deployer deployer; + + @Before + public void deployScripts() throws Exception { + deployer.deploy(SCRIPT_DEPLOYMENT_NAME); + reconnectAdminClient(); + } + + @Before + public void cleanMappersAndScopes() { + this.cau = ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_EMPLOYEE_2) + .setDefaultClientScopes(Collections.EMPTY_LIST) + .update(); + this.pmu = cau.protocolMappers() + .clear() + .update(); + + getCleanup(REALM_NAME) + .addCleanup(this.cau) + .addCleanup(this.pmu); + } + + @After + public void onAfter() throws Exception { + deployer.undeploy(SCRIPT_DEPLOYMENT_NAME); + reconnectAdminClient(); + } + + @Test + public void testScriptMapperNotAvailableThroughAdminRest() { + assertFalse(adminClient.serverInfo().getInfo().getProtocolMapperTypes().get(SamlProtocol.LOGIN_PROTOCOL).stream() + .anyMatch( + mapper -> ScriptBasedMapper.PROVIDER_ID.equals(mapper.getId()))); + + // Doublecheck not possible to create mapper through admin REST + ProtocolMapperRepresentation mapperRep = createSamlProtocolMapper(ScriptBasedMapper.PROVIDER_ID, + ProviderConfigProperty.SCRIPT_TYPE, "'hello_' + user.username", + AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, AttributeStatementHelper.BASIC, + AttributeStatementHelper.SAML_ATTRIBUTE_NAME, "SCRIPT_ATTRIBUTE" + ); + + Response response = pmu.getResource().createMapper(mapperRep); + Assert.assertEquals(404, response.getStatus()); + response.close(); + } + + + @Test + @EnableFeature(value = SCRIPTS, skipRestart = true, executeAsLast = false) + public void testScriptMappingThroughServerDeploy() { + // ScriptBasedMapper still not available even if SCRIPTS feature is enabled + testScriptMapperNotAvailableThroughAdminRest(); + + pmu.add( + createSamlProtocolMapper("script-mapper-a.js", + AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, AttributeStatementHelper.BASIC, + AttributeStatementHelper.SAML_ATTRIBUTE_NAME, "SCRIPT_ATTRIBUTE" + ) + ).update(); + + assertLoginSuccessWithAttributeAvailable(); + } + + + private void assertLoginSuccessWithAttributeAvailable() { + SAMLDocumentHolder samlResponse = new SamlClientBuilder() + .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_EMPLOYEE_2, RoleMapperTest.SAML_ASSERTION_CONSUMER_URL_EMPLOYEE_2, SamlClient.Binding.POST) + .build() + .login().user(bburkeUser).build() + .getSamlResponse(SamlClient.Binding.POST); + + assertThat(samlResponse.getSamlObject(), Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); + + Stream<AssertionType> assertions = assertionsUnencrypted(samlResponse.getSamlObject()); + Stream<AttributeType> attributes = attributesUnecrypted(attributeStatements(assertions)); + String scriptAttrValue = attributes + .filter(attribute -> "SCRIPT_ATTRIBUTE".equals(attribute.getName())) + .map(attribute -> attribute.getAttributeValue().get(0).toString()) + .findFirst().orElseThrow(() -> new AssertionError("Attribute SCRIPT_ATTRIBUTE was not available in SAML assertion")); + + Assert.assertEquals("hello_bburke", scriptAttrValue); + } + +}
testsuite/utils/src/main/java/org/keycloak/testsuite/KeycloakServer.java+4 −0 modified@@ -40,6 +40,7 @@ import org.keycloak.platform.Platform; import org.keycloak.protocol.ProtocolMapperSpi; import org.keycloak.protocol.oidc.mappers.DeployedScriptOIDCProtocolMapper; +import org.keycloak.protocol.saml.mappers.DeployedScriptSAMLProtocolMapper; import org.keycloak.provider.KeycloakDeploymentInfo; import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderManager; @@ -601,6 +602,9 @@ public static void registerScriptProviders(DefaultKeycloakSessionFactory session addScriptProvider(info, scriptProviderDescriptor.getProviders().getOrDefault("mappers", Collections.emptyList()), ProtocolMapperSpi.class, DeployedScriptOIDCProtocolMapper::new); + addScriptProvider(info, scriptProviderDescriptor.getProviders().getOrDefault("saml-mappers", Collections.emptyList()), + ProtocolMapperSpi.class, + DeployedScriptSAMLProtocolMapper::new); addScriptProvider(info, scriptProviderDescriptor.getProviders().getOrDefault("policies", Collections.emptyList()), PolicySpi.class, DeployedScriptPolicyFactory::new);
wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ScriptProviderDeploymentProcessor.java+7 −0 modified@@ -19,6 +19,7 @@ import static org.keycloak.representations.provider.ScriptProviderDescriptor.AUTHENTICATORS; import static org.keycloak.representations.provider.ScriptProviderDescriptor.MAPPERS; import static org.keycloak.representations.provider.ScriptProviderDescriptor.POLICIES; +import static org.keycloak.representations.provider.ScriptProviderDescriptor.SAML_MAPPERS; import java.io.IOException; import java.io.InputStream; @@ -39,6 +40,7 @@ import org.keycloak.common.util.StreamUtil; import org.keycloak.protocol.ProtocolMapperSpi; import org.keycloak.protocol.oidc.mappers.DeployedScriptOIDCProtocolMapper; +import org.keycloak.protocol.saml.mappers.DeployedScriptSAMLProtocolMapper; import org.keycloak.provider.KeycloakDeploymentInfo; import org.keycloak.representations.provider.ScriptProviderDescriptor; import org.keycloak.representations.provider.ScriptProviderMetadata; @@ -63,6 +65,10 @@ private static void registerScriptMapper(KeycloakDeploymentInfo info, ScriptProv info.addProvider(ProtocolMapperSpi.class, new DeployedScriptOIDCProtocolMapper(metadata)); } + private static void registerSAMLScriptMapper(KeycloakDeploymentInfo info, ScriptProviderMetadata metadata) { + info.addProvider(ProtocolMapperSpi.class, new DeployedScriptSAMLProtocolMapper(metadata)); + } + static void deploy(DeploymentUnit deploymentUnit, KeycloakDeploymentInfo info) { ResourceRoot resourceRoot = deploymentUnit.getAttachment(Attachments.DEPLOYMENT_ROOT); @@ -129,5 +135,6 @@ private static ScriptProviderDescriptor readScriptProviderDescriptor(VirtualFile PROVIDERS.put(AUTHENTICATORS, ScriptProviderDeploymentProcessor::registerScriptAuthenticator); PROVIDERS.put(POLICIES, ScriptProviderDeploymentProcessor::registerScriptPolicy); PROVIDERS.put(MAPPERS, ScriptProviderDeploymentProcessor::registerScriptMapper); + PROVIDERS.put(SAML_MAPPERS, ScriptProviderDeploymentProcessor::registerSAMLScriptMapper); } }
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-wf7g-7h6h-678vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-2668ghsaADVISORY
- access.redhat.com/security/cve/CVE-2022-2668ghsax_refsource_MISCWEB
- bugzilla.redhat.com/show_bug.cgighsaWEB
- github.com/keycloak/keycloak/commit/e2ae7eef39b27e48ffa4764995d558555f02838cghsaWEB
- github.com/keycloak/keycloak/security/advisories/GHSA-wf7g-7h6h-678vghsaWEB
News mentions
0No linked articles in our index yet.