[26.2] Only allow LDAP URL references when following referrals (#286)

* Only allow LDAP URL references when following referrals

Closes #280

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>

* Updating docs

Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>

* Adjusting CI for slowness

Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>

---------

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>
Co-authored-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Alexander Schwartz
2025-11-21 11:20:33 +01:00
committed by GitHub
parent 32e24dff6c
commit b90fec41ff
8 changed files with 195 additions and 1 deletions

View File

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

View File

@@ -6,6 +6,7 @@ on:
- main
- dependabot/**
- quarkus-next
- issue*
pull_request:
workflow_dispatch:

View File

@@ -5,6 +5,7 @@ on:
branches-ignore:
- main
- dependabot/**
- issue*
pull_request:
workflow_dispatch:

View File

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

View File

@@ -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].

View File

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

View File

@@ -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 <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 class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
@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 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
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);
}
}
}

View File

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