Medium severity5.5OSV Advisory· Published Nov 25, 2025· Updated Apr 15, 2026
CVE-2025-13467
CVE-2025-13467
Description
A flaw was found in the Keycloak LDAP User Federation provider. This vulnerability allows an authenticated realm administrator to trigger deserialization of untrusted Java objects via a malicious LDAP server configuration.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.keycloak:keycloak-ldap-federationMaven | >= 26.3.0, < 26.4.6 | 26.4.6 |
org.keycloak:keycloak-ldap-federationMaven | < 26.2.11 | 26.2.11 |
Affected products
1Patches
2754c070cf8ca[26.4] Only allow LDAP URL references when following referrals (#285)
11 files changed · +207 −3
docs/documentation/release_notes/index.adoc+3 −0 modified@@ -13,6 +13,9 @@ include::topics/templates/document-attributes.adoc[] :release_header_latest_link: {releasenotes_link_latest} include::topics/templates/release-header.adoc[] +== {project_name_full} 26.4.6 +include::topics/26_4_6.adoc[leveloffset=2] + == {project_name_full} 26.4.0 include::topics/26_4_0.adoc[leveloffset=2]
docs/documentation/release_notes/topics/26_4_6.adoc+9 −0 added@@ -0,0 +1,9 @@ +// Release notes should contain only headline-worthy new features, +// assuming that people who migrate will read the upgrading guide anyway. + +This release adds filtering of LDAP referrals by default. +This change enhances security and aligns with best practices for LDAP configurations. + +If you can not upgrade to this release yet, we recommend disabling LDAP referrals in all LDAP providers in all of your realms. + +For detailed upgrade instructions, https://www.keycloak.org/docs/latest/upgrading/index.html[review the upgrading guide].
docs/documentation/upgrading/topics/changes/changes-26_4_6.adoc+21 −0 added@@ -0,0 +1,21 @@ +// ------------------------ Breaking changes ------------------------ // +== Notable changes + +Notable changes may include internal behavior changes that prevent common misconfigurations, bugs that are fixed, or changes to simplify running {project_name}. + +=== LDAP referrals filtered to allow only LDAP referrals + +LDAP referrals now by default are only allowed to include LDAP URLs. +This change enhances security and aligns with best practices for LDAP configurations. + +This also prevents other JDNI references from being used in case you have written custom extensions. +To restore the original behavior, set the option `spi-storage--ldap--secure-referral` to `false`. +When doing this, we recommend to disable LDAP referrals in all LDAP providers. + +== Deprecated features + +The following sections provide details on deprecated features. + +=== Disabling filtering of LDAP referrals + +The option `spi-storage--ldap--secure-referral` to disable filtering referrals is deprecated. It will be removed in a future release and filtering will then be enforced.
docs/documentation/upgrading/topics/changes/changes.adoc+4 −0 modified@@ -1,6 +1,10 @@ [[migration-changes]] == Migration Changes +=== Migrating to 26.4.6 + +include::changes-26_4_6.adoc[leveloffset=2] + === Migrating to 26.4.3 include::changes-26_4_3.adoc[leveloffset=2]
federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java+39 −0 modified@@ -73,6 +73,9 @@ import java.util.function.Function; import java.util.stream.Collectors; +import javax.naming.NamingException; +import javax.naming.spi.NamingManager; + /** * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> @@ -84,6 +87,8 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD private static final Logger logger = Logger.getLogger(LDAPStorageProviderFactory.class); public static final String PROVIDER_NAME = LDAPConstants.LDAP_PROVIDER; private static final String LDAP_CONNECTION_POOL_PROTOCOL = "com.sun.jndi.ldap.connect.pool.protocol"; + private static final String SECURE_REFERRAL = "secureReferral"; + private static final boolean SECURE_REFERRAL_DEFAULT = true; private LDAPIdentityStoreRegistry ldapStoreRegistry; @@ -301,13 +306,36 @@ public void validateConfiguration(KeycloakSession session, RealmModel realm, Com @Override public void init(Config.Scope config) { + if (config.getBoolean(SECURE_REFERRAL, SECURE_REFERRAL_DEFAULT)) { + setObjectFactoryBuilder(); + } else { + logger.warnf("Insecure LDAP referrals are enabled. The option 'secure-referral' is deprecated and it will be removed in future releases."); + } + // set connection pooling for plain and tls protocols by default if (System.getProperty(LDAP_CONNECTION_POOL_PROTOCOL) == null) { System.setProperty(LDAP_CONNECTION_POOL_PROTOCOL, "plain ssl"); } + this.ldapStoreRegistry = new LDAPIdentityStoreRegistry(); } + @Override + public List<ProviderConfigProperty> getConfigMetadata() { + + ProviderConfigurationBuilder builder = ProviderConfigurationBuilder.create(); + + builder.property() + .name(SECURE_REFERRAL) + .type("boolean") + .helpText("Allow only secure LDAP referrals (deprecated)") + .defaultValue(SECURE_REFERRAL_DEFAULT) + .add(); + + return builder.build(); + + } + @Override public void close() { this.ldapStoreRegistry = null; @@ -727,4 +755,15 @@ protected KerberosUsernamePasswordAuthenticator createKerberosUsernamePasswordAu return new KerberosUsernamePasswordAuthenticator(kerberosConfig); } + private void setObjectFactoryBuilder() { + try { + NamingManager.setObjectFactoryBuilder(new ObjectFactoryBuilder()); + } catch (NamingException | IllegalStateException e) { + if (e instanceof IllegalStateException && ObjectFactoryBuilder.isSet()) { + return; + } + + throw new RuntimeException("Failed to set the server JNDI ObjectFactoryBuilder", e); + } + } }
federation/ldap/src/main/java/org/keycloak/storage/ldap/ObjectFactoryBuilder.java+124 −0 added@@ -0,0 +1,124 @@ +package org.keycloak.storage.ldap; + +import java.util.Hashtable; +import java.util.List; +import javax.naming.CommunicationException; +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.ldap.LdapContext; +import javax.naming.spi.NamingManager; +import javax.naming.spi.ObjectFactory; + +import org.jboss.logging.Logger; +import org.keycloak.storage.ldap.idm.store.ldap.SessionBoundInitialLdapContext; +import org.keycloak.utils.KeycloakSessionUtil; + +/** + * <p>A {@link javax.naming.spi.ObjectFactoryBuilder} implementation to filter out referral references if they do not + * point to an LDAP URL. + * + * <p>When the LDAP provider encounters a referral, it tries to create an {@link ObjectFactory} from this builder. + * If the referral reference contains an LDAP URL, a {@link DirContextObjectFactory} is created to handle the referral. + * Otherwise, a {@link CommunicationException} is thrown to indicate that the referral cannot be processed. + */ +final class ObjectFactoryBuilder implements javax.naming.spi.ObjectFactoryBuilder, ObjectFactory { + + private static final Logger logger = Logger.getLogger(ObjectFactoryBuilder.class); + private static final String IS_KC_OBJECT_FACTORY_BUILDER = "kc.jndi.object.factory.builder"; + + static boolean isSet() { + Hashtable<Object, Object> env = new Hashtable<>(); + + env.put(ObjectFactoryBuilder.IS_KC_OBJECT_FACTORY_BUILDER, Boolean.TRUE); + + try { + Object instance = NamingManager.getObjectInstance(null, null, null, env); + + if (instance != null && instance.getClass().getName().equals(ObjectFactoryBuilder.class.getName())) { + return true; + } + } catch (Exception e) { + throw new RuntimeException("Failed to determine if ObjectFactoryBuilder is set", e); + } + + return false; + } + + @Override + public ObjectFactory createObjectFactory(Object obj, Hashtable<?, ?> environment) throws NamingException { + if (logger.isTraceEnabled()) { + logger.tracef("Creating ObjectFactory for object: %s", obj); + } + + if (obj instanceof Reference ref) { + String factoryClassName = ref.getFactoryClassName(); + + if (factoryClassName != null) { + logger.warnf("Referral refence contains an object factory %s but it will be ignored", factoryClassName); + } + + String ldapUrl = getLdapUrl(ref); + + if (ldapUrl != null) { + return new DirContextObjectFactory(ldapUrl); + } + } else { + logger.debugf("Unsupported reference object of type %s: ", obj); + return this; + } + + throw new CommunicationException("Referral reference does not contain an LDAP URL: " + obj); + } + + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> env) { + if (env != null && env.containsKey(IS_KC_OBJECT_FACTORY_BUILDER)) { + return this; + } + return obj; + } + + private String getLdapUrl(Reference ref) { + for (int i = 0; i < ref.size(); i++) { + RefAddr addr = ref.get(i); + String addrType = addr.getType(); + + if ("URL".equalsIgnoreCase(addrType)) { + Object content = addr.getContent(); + + if (content == null) { + return null; + } + + String rawUrl = content.toString(); + + for (String url : List.of(rawUrl.split(" "))) { + if (!url.toLowerCase().startsWith("ldap")) { + logger.warnf("Unsupported scheme from reference URL %s. Ignoring reference.", url); + return null; + } + } + + return rawUrl; + } else { + logger.warnf("Ignoring address of type '%s' from referral reference", addrType); + } + } + + return null; + } + + private record DirContextObjectFactory(String ldapUrl) implements ObjectFactory { + + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> env) throws Exception { + @SuppressWarnings("unchecked") + Hashtable<Object, Object> newEnv = (Hashtable<Object, Object>) env.clone(); + newEnv.put(LdapContext.PROVIDER_URL, ldapUrl); + return new SessionBoundInitialLdapContext(KeycloakSessionUtil.getKeycloakSession(), newEnv, null); + } + } +}
.github/workflows/ci.yml+3 −2 modified@@ -5,6 +5,7 @@ on: branches-ignore: - main - dependabot/** + - issue* pull_request: workflow_dispatch: @@ -573,7 +574,7 @@ jobs: name: Store IT needs: build runs-on: ubuntu-latest - timeout-minutes: 75 + timeout-minutes: 90 strategy: matrix: db: [postgres, mysql, oracle, mssql, mariadb, tidb] @@ -871,7 +872,7 @@ jobs: runs-on: ubuntu-latest needs: - build - timeout-minutes: 45 + timeout-minutes: 60 steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
.github/workflows/documentation.yml+1 −0 modified@@ -6,6 +6,7 @@ on: - main - dependabot/** - quarkus-next + - issue* pull_request: workflow_dispatch:
.github/workflows/js-ci.yml+1 −0 modified@@ -6,6 +6,7 @@ on: - main - dependabot/** - quarkus-next + - issue* pull_request: workflow_dispatch:
.github/workflows/operator-ci.yml+1 −0 modified@@ -5,6 +5,7 @@ on: branches-ignore: - main - dependabot/** + - issue* pull_request: workflow_dispatch:
quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ImportDistTest.java+1 −1 modified@@ -85,7 +85,7 @@ void testImportLargeUserCount(KeycloakDistribution dist) throws Exception { ExecutorService ex = Executors.newFixedThreadPool(1); Future<CLIResult> result = ex.submit(() -> dist.run("import", "--dir=" + dir.getAbsolutePath())); try { - cliResult = result.get(20, TimeUnit.SECONDS); + cliResult = result.get(40, TimeUnit.SECONDS); cliResult.assertMessage("Realm 'master' imported"); cliResult.assertMessage("Import finished successfully"); cliResult.assertMessage("master-users-0.json");
b90fec41ff17[26.2] Only allow LDAP URL references when following referrals (#286)
8 files changed · +195 −1
docs/documentation/release_notes/index.adoc+3 −0 modified@@ -13,6 +13,9 @@ include::topics/templates/document-attributes.adoc[] :release_header_latest_link: {releasenotes_link_latest} include::topics/templates/release-header.adoc[] +== {project_name_full} 26.2.11 +include::topics/26_2_11.adoc[leveloffset=2] + == {project_name_full} 26.2.6 include::topics/26_2_6.adoc[leveloffset=2]
docs/documentation/release_notes/topics/26_2_11.adoc+9 −0 added@@ -0,0 +1,9 @@ +// Release notes should contain only headline-worthy new features, +// assuming that people who migrate will read the upgrading guide anyway. + +This release adds filtering of LDAP referrals by default. +This change enhances security and aligns with best practices for LDAP configurations. + +If you can not upgrade to this release yet, we recomment to disable LDAP referrals in all LDAP providers in all of your realms. + +For detailed upgrade instructions, https://www.keycloak.org/docs/latest/upgrading/index.html[review the upgrading guide].
docs/documentation/upgrading/topics/changes/changes-26_2_11.adoc+16 −0 modified@@ -16,3 +16,19 @@ This adds new indexes on `OFFLINE_CLIENT_SESSION` table to improve performance w If those tables contain more than 300000 entries, {project_name} will skip the index creation by default during the automatic schema migration and instead log the SQL statement on the console during migration to be applied manually after {project_name}'s startup. See the link:{upgradingguide_link}[{upgradingguide_name}] for details on how to configure a different limit. +=== LDAP referrals filtered to allow only LDAP referrals + +LDAP referrals now by default are only allowed to include LDAP URLs. +This change enhances security and aligns with best practices for LDAP configurations. + +This also prevents other JDNI references from being used in case you have written custom extensions. +To restore the original behavior, set the option `spi-storage--ldap--secure-referral` to `false`. +When doing this, we recommend to disable LDAP referrals in all LDAP providers. + +== Deprecated features + +The following sections provide details on deprecated features. + +=== Disabling filtering of LDAP referrals + +The option `spi-storage--ldap--secure-referral` to disable filtering referrals is deprecated. It will be removed in a future release and filtering will then be enforced.
federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java+39 −0 modified@@ -73,6 +73,9 @@ import java.util.function.Function; import java.util.stream.Collectors; +import javax.naming.NamingException; +import javax.naming.spi.NamingManager; + /** * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> @@ -84,6 +87,8 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD private static final Logger logger = Logger.getLogger(LDAPStorageProviderFactory.class); public static final String PROVIDER_NAME = LDAPConstants.LDAP_PROVIDER; private static final String LDAP_CONNECTION_POOL_PROTOCOL = "com.sun.jndi.ldap.connect.pool.protocol"; + private static final String SECURE_REFERRAL = "secureReferral"; + private static final boolean SECURE_REFERRAL_DEFAULT = true; private LDAPIdentityStoreRegistry ldapStoreRegistry; @@ -301,13 +306,36 @@ public void validateConfiguration(KeycloakSession session, RealmModel realm, Com @Override public void init(Config.Scope config) { + if (config.getBoolean(SECURE_REFERRAL, SECURE_REFERRAL_DEFAULT)) { + setObjectFactoryBuilder(); + } else { + logger.warnf("Insecure LDAP referrals are enabled. The option 'secure-referral' is deprecated and it will be removed in future releases."); + } + // set connection pooling for plain and tls protocols by default if (System.getProperty(LDAP_CONNECTION_POOL_PROTOCOL) == null) { System.setProperty(LDAP_CONNECTION_POOL_PROTOCOL, "plain ssl"); } + this.ldapStoreRegistry = new LDAPIdentityStoreRegistry(); } + @Override + public List<ProviderConfigProperty> getConfigMetadata() { + + ProviderConfigurationBuilder builder = ProviderConfigurationBuilder.create(); + + builder.property() + .name(SECURE_REFERRAL) + .type("boolean") + .helpText("Allow only secure LDAP referrals (deprecated)") + .defaultValue(SECURE_REFERRAL_DEFAULT) + .add(); + + return builder.build(); + + } + @Override public void close() { this.ldapStoreRegistry = null; @@ -727,4 +755,15 @@ protected KerberosUsernamePasswordAuthenticator createKerberosUsernamePasswordAu return new KerberosUsernamePasswordAuthenticator(kerberosConfig); } + private void setObjectFactoryBuilder() { + try { + NamingManager.setObjectFactoryBuilder(new ObjectFactoryBuilder()); + } catch (NamingException | IllegalStateException e) { + if (e instanceof IllegalStateException && ObjectFactoryBuilder.isSet()) { + return; + } + + throw new RuntimeException("Failed to set the server JNDI ObjectFactoryBuilder", e); + } + } }
federation/ldap/src/main/java/org/keycloak/storage/ldap/ObjectFactoryBuilder.java+124 −0 added@@ -0,0 +1,124 @@ +package org.keycloak.storage.ldap; + +import java.util.Hashtable; +import java.util.List; +import javax.naming.CommunicationException; +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.ldap.LdapContext; +import javax.naming.spi.NamingManager; +import javax.naming.spi.ObjectFactory; + +import org.jboss.logging.Logger; +import org.keycloak.storage.ldap.idm.store.ldap.SessionBoundInitialLdapContext; +import org.keycloak.utils.KeycloakSessionUtil; + +/** + * <p>A {@link javax.naming.spi.ObjectFactoryBuilder} implementation to filter out referral references if they do not + * point to an LDAP URL. + * + * <p>When the LDAP provider encounters a referral, it tries to create an {@link ObjectFactory} from this builder. + * If the referral reference contains an LDAP URL, a {@link DirContextObjectFactory} is created to handle the referral. + * Otherwise, a {@link CommunicationException} is thrown to indicate that the referral cannot be processed. + */ +final class ObjectFactoryBuilder implements javax.naming.spi.ObjectFactoryBuilder, ObjectFactory { + + private static final Logger logger = Logger.getLogger(ObjectFactoryBuilder.class); + private static final String IS_KC_OBJECT_FACTORY_BUILDER = "kc.jndi.object.factory.builder"; + + static boolean isSet() { + Hashtable<Object, Object> env = new Hashtable<>(); + + env.put(ObjectFactoryBuilder.IS_KC_OBJECT_FACTORY_BUILDER, Boolean.TRUE); + + try { + Object instance = NamingManager.getObjectInstance(null, null, null, env); + + if (instance != null && instance.getClass().getName().equals(ObjectFactoryBuilder.class.getName())) { + return true; + } + } catch (Exception e) { + throw new RuntimeException("Failed to determine if ObjectFactoryBuilder is set", e); + } + + return false; + } + + @Override + public ObjectFactory createObjectFactory(Object obj, Hashtable<?, ?> environment) throws NamingException { + if (logger.isTraceEnabled()) { + logger.tracef("Creating ObjectFactory for object: %s", obj); + } + + if (obj instanceof Reference ref) { + String factoryClassName = ref.getFactoryClassName(); + + if (factoryClassName != null) { + logger.warnf("Referral refence contains an object factory %s but it will be ignored", factoryClassName); + } + + String ldapUrl = getLdapUrl(ref); + + if (ldapUrl != null) { + return new DirContextObjectFactory(ldapUrl); + } + } else { + logger.debugf("Unsupported reference object of type %s: ", obj); + return this; + } + + throw new CommunicationException("Referral reference does not contain an LDAP URL: " + obj); + } + + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> env) { + if (env != null && env.containsKey(IS_KC_OBJECT_FACTORY_BUILDER)) { + return this; + } + return obj; + } + + private String getLdapUrl(Reference ref) { + for (int i = 0; i < ref.size(); i++) { + RefAddr addr = ref.get(i); + String addrType = addr.getType(); + + if ("URL".equalsIgnoreCase(addrType)) { + Object content = addr.getContent(); + + if (content == null) { + return null; + } + + String rawUrl = content.toString(); + + for (String url : List.of(rawUrl.split(" "))) { + if (!url.toLowerCase().startsWith("ldap")) { + logger.warnf("Unsupported scheme from reference URL %s. Ignoring reference.", url); + return null; + } + } + + return rawUrl; + } else { + logger.warnf("Ignoring address of type '%s' from referral reference", addrType); + } + } + + return null; + } + + private record DirContextObjectFactory(String ldapUrl) implements ObjectFactory { + + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> env) throws Exception { + @SuppressWarnings("unchecked") + Hashtable<Object, Object> newEnv = (Hashtable<Object, Object>) env.clone(); + newEnv.put(LdapContext.PROVIDER_URL, ldapUrl); + return new SessionBoundInitialLdapContext(KeycloakSessionUtil.getKeycloakSession(), newEnv, null); + } + } +}
.github/workflows/ci.yml+2 −1 modified@@ -5,6 +5,7 @@ on: branches-ignore: - main - dependabot/** + - issue* pull_request: workflow_dispatch: @@ -1089,7 +1090,7 @@ jobs: runs-on: ubuntu-latest needs: - build - timeout-minutes: 30 + timeout-minutes: 60 steps: - uses: actions/checkout@v4
.github/workflows/js-ci.yml+1 −0 modified@@ -6,6 +6,7 @@ on: - main - dependabot/** - quarkus-next + - issue* pull_request: workflow_dispatch:
.github/workflows/operator-ci.yml+1 −0 modified@@ -5,6 +5,7 @@ on: branches-ignore: - main - dependabot/** + - issue* pull_request: workflow_dispatch:
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
13- github.com/advisories/GHSA-4hx9-48xh-5mxrghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-13467ghsaADVISORY
- access.redhat.com/errata/RHSA-2025:22088nvdWEB
- access.redhat.com/security/cve/CVE-2025-13467nvdWEB
- bugzilla.redhat.com/show_bug.cginvdWEB
- github.com/keycloak/keycloak/commit/754c070cf8ca187dcc71f0f72ff3130ff2195328nvdWEB
- github.com/keycloak/keycloak/commit/b90fec41ff17a70858d830750156a8a2e13ddb82ghsaWEB
- github.com/keycloak/keycloak/issues/44478nvdWEB
- github.com/keycloak/keycloak/releases/tag/26.4.6ghsaWEB
- github.com/keycloak/keycloak/security/advisories/GHSA-4hx9-48xh-5mxrghsaWEB
- access.redhat.com/errata/RHSA-2025:22089nvd
- access.redhat.com/errata/RHSA-2025:22090nvd
- access.redhat.com/errata/RHSA-2025:22091nvd
News mentions
0No linked articles in our index yet.