diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22ebee048f8..82feaa8998b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/js-ci.yml b/.github/workflows/js-ci.yml index 7855c5d8194..e9b611b564b 100644 --- a/.github/workflows/js-ci.yml +++ b/.github/workflows/js-ci.yml @@ -6,6 +6,7 @@ on: - main - dependabot/** - quarkus-next + - issue* pull_request: workflow_dispatch: diff --git a/.github/workflows/operator-ci.yml b/.github/workflows/operator-ci.yml index a595399b232..504a718d6a1 100644 --- a/.github/workflows/operator-ci.yml +++ b/.github/workflows/operator-ci.yml @@ -5,6 +5,7 @@ on: branches-ignore: - main - dependabot/** + - issue* pull_request: workflow_dispatch: diff --git a/docs/documentation/release_notes/index.adoc b/docs/documentation/release_notes/index.adoc index 9e5fb363316..f53f4cc5cb9 100644 --- a/docs/documentation/release_notes/index.adoc +++ b/docs/documentation/release_notes/index.adoc @@ -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] diff --git a/docs/documentation/release_notes/topics/26_2_11.adoc b/docs/documentation/release_notes/topics/26_2_11.adoc new file mode 100644 index 00000000000..57b213f7d34 --- /dev/null +++ b/docs/documentation/release_notes/topics/26_2_11.adoc @@ -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]. diff --git a/docs/documentation/upgrading/topics/changes/changes-26_2_11.adoc b/docs/documentation/upgrading/topics/changes/changes-26_2_11.adoc index dff61e5d25b..f5f6382aad8 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_2_11.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_2_11.adoc @@ -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. diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java index a6dacb20d2d..152ddf8f7c1 100755 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java @@ -73,6 +73,9 @@ import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; +import javax.naming.NamingException; +import javax.naming.spi.NamingManager; + /** * @author Marek Posolda * @author Bill Burke @@ -84,6 +87,8 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory 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 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactoryA {@link javax.naming.spi.ObjectFactoryBuilder} implementation to filter out referral references if they do not + * point to an LDAP URL. + * + *

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 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 newEnv = (Hashtable) env.clone(); + newEnv.put(LdapContext.PROVIDER_URL, ldapUrl); + return new SessionBoundInitialLdapContext(KeycloakSessionUtil.getKeycloakSession(), newEnv, null); + } + } +}