Only allow LDAP URL references when following referrals (#44993)

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>
Signed-off-by: Stian Thorgersen <stian@redhat.com>
Co-authored-by: Alexander Schwartz <alexander.schwartz@ibm.com>
Co-authored-by: Stian Thorgersen <stianst@gmail.com>
This commit is contained in:
Pedro Igor 2025-12-18 10:27:10 -03:00 committed by GitHub
parent 94dc60822b
commit 6a437521a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 175 additions and 0 deletions

View File

@ -16,6 +16,9 @@ include::topics/templates/release-header.adoc[]
== {project_name_full} 26.5.0
include::topics/26_5_0.adoc[leveloffset=2]
== {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]

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

View File

@ -23,6 +23,8 @@ import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.naming.NamingException;
import javax.naming.spi.NamingManager;
import org.keycloak.Config;
import org.keycloak.common.constants.KerberosConstants;
@ -85,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;
@ -302,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;
@ -728,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,125 @@
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.keycloak.storage.ldap.idm.store.ldap.SessionBoundInitialLdapContext;
import org.keycloak.utils.KeycloakSessionUtil;
import org.jboss.logging.Logger;
/**
* <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);
}
}
}