mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-16 20:15:46 -06:00
[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:
committed by
GitHub
parent
32e24dff6c
commit
b90fec41ff
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
|
||||
1
.github/workflows/js-ci.yml
vendored
1
.github/workflows/js-ci.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
- main
|
||||
- dependabot/**
|
||||
- quarkus-next
|
||||
- issue*
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
1
.github/workflows/operator-ci.yml
vendored
1
.github/workflows/operator-ci.yml
vendored
@@ -5,6 +5,7 @@ on:
|
||||
branches-ignore:
|
||||
- main
|
||||
- dependabot/**
|
||||
- issue*
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
9
docs/documentation/release_notes/topics/26_2_11.adoc
Normal file
9
docs/documentation/release_notes/topics/26_2_11.adoc
Normal 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].
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user