import Oracle_OSS 389-ds-base-3.2.0-9.el10_2

This commit is contained in:
AlmaLinux RelEng Bot 2026-08-17 11:40:12 -04:00
parent 7241c1017b
commit af701dcc1a
4 changed files with 974 additions and 1 deletions

View File

@ -0,0 +1,874 @@
From 48e2b5f896ad021d9cda5cff409050bb37e2bf17 Mon Sep 17 00:00:00 2001
From: progier389 <progier@redhat.com>
Date: Thu, 18 Jun 2026 19:11:31 +0200
Subject: [PATCH] CVE-2026-11770 - pre-auth LDAP filter injection in CleanRUV
- Add authentication checks for all the replication extended operations
by:
- checking directly that binddn is accepted by a replica for the cleanruv
related and the start replication session extended operations:
REPL_CLEANRUV_OID
REPL_ABORT_CLEANRUV_OID
REPL_CLEANRUV_GET_MAXCSN_OID
REPL_CLEANRUV_CHECK_STATUS_OID
REPL_START_NSDS50_REPLICATION_REQUEST_OID
REPL_START_NSDS90_REPLICATION_REQUEST_OID
- Checking that an active replication session is associated with the
connection for the other session related operations:
REPL_END_NSDS50_REPLICATION_REQUEST_OID
REPL_NSDS50_REPLICATION_ENTRY_REQUEST_OID
REPL_NSDS71_REPLICATION_ENTRY_REQUEST_OID
Note about REPL_END_NSDS50_REPLICATION_REQUEST_OID:
if there is no replication session, the operation
does nothing else than sending success response
- Implement LDAP injection protection for REPL_CLEANRUV_CHECK_STATUS_OID operation
- Add comprehensive test suite (38+ test cases)
- Fix isdigit() undefined behavior for non-ASCII characters
- Designed a slower but safer way to iterate on the replicas to
avoid deadlocks within the callbacks
Security impact:
- Prevents LDAP injection via malformed CleanAllRUV filters
- Blocks unauthorized access to cleanruv related extended
operations
- Blocks anonymous access to replication session related
extended operations
Test coverage:
- All replication extended operation OIDs
- Anonymous, unauthorized, and authorized access
- LDAP injection attempts
- Dynamic authorization revocation
Assisted-by: Claude Ai
---
.../test_cleanruv_extop_security.py | 452 ++++++++++++++++++
ldap/servers/plugins/replication/repl5.h | 2 +
.../replication/repl5_replica_config.c | 72 +++
.../servers/plugins/replication/repl5_total.c | 13 +
ldap/servers/plugins/replication/repl_extop.c | 167 +++++++
5 files changed, 706 insertions(+)
create mode 100644 dirsrvtests/tests/suites/replication/test_cleanruv_extop_security.py
diff --git a/dirsrvtests/tests/suites/replication/test_cleanruv_extop_security.py b/dirsrvtests/tests/suites/replication/test_cleanruv_extop_security.py
new file mode 100644
index 000000000..041347861
--- /dev/null
+++ b/dirsrvtests/tests/suites/replication/test_cleanruv_extop_security.py
@@ -0,0 +1,452 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2026 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+"""
+Test the security of CleanAllRUV extended operations.
+
+This test suite verifies that:
+1. Anonymous connections cannot send CleanAllRUV extended operations
+2. Unauthorized users cannot send CleanAllRUV extended operations
+3. LDAP injection via malformed filter payloads is blocked
+4. Only authorized replication bind DNs can use these operations
+5. All 4 replication extended operations enforce security checks:
+ - CleanAllRUV (2.16.840.1.113730.3.6.5)
+ - Abort CleanAllRUV (2.16.840.1.113730.3.6.6)
+ - Get MaxCSN (2.16.840.1.113730.3.6.7)
+ - Check Status (2.16.840.1.113730.3.6.8)
+6. Authorization is properly revoked when user is removed from replication group
+"""
+
+import os
+import time
+import ldap
+import pytest
+from contextlib import contextmanager
+from lib389._constants import DEFAULT_SUFFIX, PASSWORD
+from lib389.idm.group import Groups
+from lib389.idm.user import UserAccounts
+from pyasn1.codec.ber import encoder
+from pyasn1.type import univ
+from test389.topologies import topology_m2
+
+pytestmark = pytest.mark.tier1
+
+import logging
+log = logging.getLogger(__name__)
+
+DEBUGGING = os.getenv('DEBUGGING', default=False)
+
+# Extended operation OIDs
+REPL_CLEANRUV_CHECK_STATUS_OID = "2.16.840.1.113730.3.6.8"
+REPL_CLEANRUV_GET_MAXCSN_OID = "2.16.840.1.113730.3.6.7"
+REPL_CLEANRUV_OID = "2.16.840.1.113730.3.6.5"
+REPL_ABORT_CLEANRUV_OID = "2.16.840.1.113730.3.6.6"
+REPL_SESSION_END_OID = "2.16.840.1.113730.3.5.12"
+
+
+REPL_START_NSDS50_REPLICATION_REQUEST_OID = "2.16.840.1.113730.3.5.3"
+REPL_END_NSDS50_REPLICATION_REQUEST_OID = "2.16.840.1.113730.3.5.5"
+REPL_NSDS50_REPLICATION_ENTRY_REQUEST_OID = "2.16.840.1.113730.3.5.6"
+REPL_NSDS50_REPLICATION_RESPONSE_OID = "2.16.840.1.113730.3.5.4"
+REPL_NSDS50_UPDATE_INFO_CONTROL_OID = "2.16.840.1.113730.3.4.13"
+REPL_NSDS50_INCREMENTAL_PROTOCOL_OID = "2.16.840.1.113730.3.6.1"
+REPL_NSDS50_TOTAL_PROTOCOL_OID = "2.16.840.1.113730.3.6.2"
+REPL_NSDS71_INCREMENTAL_PROTOCOL_OID = "2.16.840.1.113730.3.6.4"
+REPL_NSDS71_TOTAL_PROTOCOL_OID = "2.16.840.1.113730.3.6.3"
+REPL_NSDS71_REPLICATION_ENTRY_REQUEST_OID = "2.16.840.1.113730.3.5.9"
+REPL_START_NSDS90_REPLICATION_REQUEST_OID = "2.16.840.1.113730.3.5.12"
+REPL_NSDS90_REPLICATION_RESPONSE_OID = "2.16.840.1.113730.3.5.13"
+REPL_CLEANRUV_OID = "2.16.840.1.113730.3.6.5"
+REPL_ABORT_CLEANRUV_OID = "2.16.840.1.113730.3.6.6"
+REPL_CLEANRUV_GET_MAXCSN_OID = "2.16.840.1.113730.3.6.7"
+REPL_CLEANRUV_CHECK_STATUS_OID = "2.16.840.1.113730.3.6.8"
+REPL_ABORT_SESSION_OID = "2.16.840.1.113730.3.6.9"
+
+# Delay (in seconds) need to resynchronized the group cache
+REPL_GROUP_SYNC_DELAY = 30.0
+
+# Bind DN types for parametrization
+BIND_ANONYMOUS = "anonymous"
+BIND_UNAUTHORIZED = "unauthorized"
+BIND_AUTHORIZED = "authorized"
+
+
+class AccessLogWatcher:
+ """
+ Helper class allowing to wait until a new specific extended operation
+ is completed.
+ """
+
+ @staticmethod
+ def countOp(inst, oid):
+ """
+ Count the completed extended operations having specified OID.
+ """
+ import re
+ nbops = 0
+ re1 = re.compile(fr'^.* conn=(?P<conn>[\d+])\sop=(?P<op>[\d+])\sEXT\soid="{oid}".*')
+ re2 = re.compile(r'^.* conn=(?P<conn>[\d+])\sop=(?P<op>[\d+])\sRESULT\s' +
+ r'err=(?P<err>[\d+])\stag=120\s.*')
+ saved_result = {}
+ nblines = 0
+ with open(inst.ds_paths.access_log, 'r') as fd:
+ for line in fd:
+ nblines += 1
+ result = re1.match(line)
+ if result:
+ saved_result = result.groupdict()
+ continue
+ if saved_result:
+ result = re2.match(line)
+ if result:
+ result = result.groupdict()
+ if (saved_result['op'] == result['op'] and
+ saved_result['conn'] == result['conn']):
+ saved_result = {}
+ nbops += 1
+ log.info(f'AccessLogWatcher.countOp: nbops={nbops}')
+ log.info(f'AccessLogWatcher.countOp: nblines={nblines}')
+ return nbops
+
+ def __init__(self, inst, oid):
+ self._inst = inst
+ self._oid = oid
+ self._nbops = AccessLogWatcher.countOp(inst, oid)
+
+ def wait(self, timeout=30):
+ """Wait for a new extended operation with the specified OID to complete."""
+ start_time = time.time()
+ while time.time() - start_time < timeout:
+ if AccessLogWatcher.countOp(self._inst, self._oid) > self._nbops:
+ return
+ time.sleep(1.0)
+ assert False, f"Failed to complete {self._oid} extended operation before timeout of {timeout}s"
+
+
+def create_cleanruv_payload(filter_string):
+ """
+ Create a BER-encoded payload for CleanAllRUV extended operations.
+
+ The payload is encoded as: { string }
+ where string is an LDAP filter.
+
+ Args:
+ filter_string: The filter string to encode
+
+ Returns:
+ bytes: BER-encoded payload
+ """
+ sequence = univ.Sequence()
+ sequence.setComponentByPosition(0, univ.OctetString(filter_string))
+ return encoder.encode(sequence)
+
+
+@contextmanager
+def open_conn(inst, binddn=None, passwd=None):
+ """
+ Open ldap connection and bind.
+ """
+ conn = ldap.initialize(f"ldap://{inst.host}:{inst.port}")
+ try:
+ if binddn is not None:
+ conn.simple_bind_s(binddn, passwd)
+ yield conn
+ finally:
+ conn.unbind()
+
+
+@pytest.fixture(scope="module")
+def init_bind_user(topology_m2, request):
+ """Create test users 1 authorized and 1 not authorized and return the credentials map."""
+ supplier1 = topology_m2.ms["supplier1"]
+
+ users_accounts = UserAccounts(supplier1, DEFAULT_SUFFIX)
+ users = []
+
+ def cleanup():
+ for u in users:
+ u.delete()
+ for inst in topology_m2:
+ inst.config.set('nsslapd-accesslog-logbuffering', 'on')
+
+ for idx in range(2):
+ uid = f'bind_user{idx}'
+ if not users_accounts.exists(uid):
+ user = users_accounts.create(properties={
+ 'uid': uid,
+ 'cn': uid,
+ 'sn': f'User{idx}',
+ 'givenname': 'Bind',
+ 'userpassword': PASSWORD,
+ 'uidNumber': str(10000+idx),
+ 'gidNumber': str(10000+idx),
+ 'homeDirectory': f'/home/{uid}'
+ })
+ else:
+ user = users_accounts.get(uid)
+ users.append(user)
+ if not DEBUGGING:
+ request.addfinalizer(cleanup)
+
+ # Add users[1] in replication group
+ groups = Groups(supplier1, basedn=DEFAULT_SUFFIX, rdn=None)
+ repl_group = groups.get(dn=f'cn=replication_managers,{DEFAULT_SUFFIX}')
+ repl_group.ensure_member(users[1].dn)
+ # Ensure that group cache get updated
+ time.sleep(REPL_GROUP_SYNC_DELAY)
+ # Disable access log buffering
+ for inst in topology_m2:
+ inst.config.set('nsslapd-accesslog-logbuffering', 'off')
+
+ return {
+ BIND_ANONYMOUS: ( None, None ),
+ BIND_UNAUTHORIZED: ( users[0].dn, PASSWORD ),
+ BIND_AUTHORIZED: ( users[1].dn, PASSWORD ),
+ }
+
+def check_extop(inst, creds, extreq, bind_type, filter, expected_exception, description):
+ """
+ Open a new connection, bind as the bind_type user.
+ Perform the extended operation defined by extreq and described by filter
+ and description.
+ Then check that the expected_exception is got.
+ """
+ binddn = creds[bind_type][0]
+ bindpw = creds[bind_type][1]
+ log.info(f"Testing: {description}")
+ log.info(f" Bind type: {bind_type} - Bind DN: {binddn}")
+ log.info(f" Filter: {filter}")
+ log.info(f" Expected: {expected_exception}")
+
+ # Create connection and test
+ with open_conn(inst, binddn, bindpw) as conn:
+ log.info(f'Open new connection and bind as {bind_type} {binddn}')
+ # Attempt the extended operation
+ try:
+ result = conn.extop_s(extreq)
+ log.info(f"Extended operation succeeded. result is {result}")
+ assert expected_exception is None
+ except ldap.LDAPError as ex:
+ log.info(f"Extended operation failed with {ex}")
+ log.info(f"Expected exception is {expected_exception}")
+ assert expected_exception is not None
+ assert isinstance(ex, expected_exception)
+
+
+
+@pytest.mark.parametrize("bind_type,filter_bytes,expected_exception,test_description", [
+ # Anonymous bind tests
+ (BIND_ANONYMOUS, b"(nsds5ReplicaCleanRUV=5:1234567890:test)", ldap.INSUFFICIENT_ACCESS,
+ "Anonymous bind with valid filter for checking cleanruv task"),
+ (BIND_ANONYMOUS, b"(nsds5ReplicaAbortCleanRUV=5:dc=example,dc=com)", ldap.INSUFFICIENT_ACCESS,
+ "Anonymous bind with valid filter for checking abortion"),
+ (BIND_ANONYMOUS, b"(nsslapd-localhost=*)", ldap.INSUFFICIENT_ACCESS,
+ "Anonymous bind with injection attempt"),
+
+ # Unauthorized user tests
+ (BIND_UNAUTHORIZED, b"(nsds5ReplicaCleanRUV=5:1234567890:test)", ldap.INSUFFICIENT_ACCESS,
+ "Unauthorized user with valid filter for checking cleanruv task"),
+ (BIND_UNAUTHORIZED, b"(nsds5ReplicaAbortCleanRUV=5:dc=example,dc=com)", ldap.INSUFFICIENT_ACCESS,
+ "Unauthorized user with valid filter for checking abortion"),
+ (BIND_UNAUTHORIZED, b"(nsslapd-localhost=*)", ldap.INSUFFICIENT_ACCESS,
+ "Unauthorized user with injection attempt"),
+
+ # Authorized user with malicious filters - should reject filter, not auth
+ (BIND_AUTHORIZED, b"(nsslapd-localhost=*)", (ldap.OPERATIONS_ERROR, ldap.INSUFFICIENT_ACCESS),
+ "Injection: Server config leak"),
+ (BIND_AUTHORIZED, b"(userPassword=*)", (ldap.OPERATIONS_ERROR, ldap.INSUFFICIENT_ACCESS),
+ "Injection: Password leak attempt"),
+ (BIND_AUTHORIZED, b"(objectClass=*)", (ldap.OPERATIONS_ERROR, ldap.INSUFFICIENT_ACCESS),
+ "Injection: Overly broad search"),
+ (BIND_AUTHORIZED, b"(cn=*)", ldap.OPERATIONS_ERROR,
+ "Injection: General info leak"),
+ (BIND_AUTHORIZED, b"(&(nsds5ReplicaCleanRUV=5:*)(cn=*))", ldap.OPERATIONS_ERROR,
+ "Injection: Compound filter"),
+ (BIND_AUTHORIZED, b"(|(nsds5ReplicaCleanRUV=5:*)(userPassword=*))", ldap.OPERATIONS_ERROR,
+ "Injection: OR filter"),
+ (BIND_AUTHORIZED, b"(nsds5ReplicaCleanRUV=5:*)(cn=*)", None,
+ "Injection: Multiple filters"), # The second filter is ignored
+ (BIND_AUTHORIZED, b"", ldap.OPERATIONS_ERROR,
+ "Injection: Empty filter"),
+ (BIND_AUTHORIZED, b"invalid filter", ldap.OPERATIONS_ERROR,
+ "Injection: Invalid LDAP filter"),
+ (BIND_AUTHORIZED, b"(nsds5ReplicaCleanRUV=", (ldap.OPERATIONS_ERROR, ldap.INSUFFICIENT_ACCESS),
+ "Injection: Incomplete filter"),
+ (BIND_AUTHORIZED, b"(nsds5ReplicaCleanRUV=5:1234567890:no:0:dc=example,dc=com)", None,
+ "Valid filter for checking cleanruv task"),
+ (BIND_AUTHORIZED, b"(nsds5ReplicaAbortCleanRUV=5:dc=example,dc=com)", None,
+ "Valid filter for checking abortion"),
+])
+def test_cleanruv_extop_security(topology_m2, init_bind_user, bind_type, filter_bytes, expected_exception, test_description):
+ """Parametrized test for CleanAllRUV extended operation security.
+
+ Tests various combinations of bind DN types, filter payloads, and expected results.
+
+ :id: a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d
+ :parametrized: yes
+ :setup: Replication setup with two suppliers
+ :steps:
+ 1. Connect with specified bind DN type
+ 2. Send REPL_CLEANRUV_CHECK_STATUS extended operation with specified filter
+ 3. Verify the operation behaves as expected
+ :expectedresults:
+ 1. Connection succeeds (or not for anonymous)
+ 2. Extended operation is sent
+ 3. Server rejects with expected error or accept it as expected
+ """
+ supplier1 = topology_m2.ms["supplier1"]
+ # Determine bind credentials
+
+ # Create payload
+ payload = create_cleanruv_payload(filter_bytes)
+ log.info(f" Payload: {payload!r}")
+ extreq = ldap.extop.ExtendedRequest(REPL_CLEANRUV_CHECK_STATUS_OID, payload)
+
+ check_extop(supplier1, init_bind_user, extreq, bind_type, filter_bytes, expected_exception, test_description)
+
+
+@pytest.mark.parametrize("extop_oid", [
+ REPL_START_NSDS50_REPLICATION_REQUEST_OID,
+ REPL_END_NSDS50_REPLICATION_REQUEST_OID,
+ REPL_NSDS50_REPLICATION_ENTRY_REQUEST_OID,
+ REPL_NSDS50_REPLICATION_RESPONSE_OID,
+ REPL_NSDS50_UPDATE_INFO_CONTROL_OID,
+ REPL_NSDS50_INCREMENTAL_PROTOCOL_OID,
+ REPL_NSDS50_TOTAL_PROTOCOL_OID,
+ REPL_NSDS71_INCREMENTAL_PROTOCOL_OID,
+ REPL_NSDS71_TOTAL_PROTOCOL_OID,
+ REPL_NSDS71_REPLICATION_ENTRY_REQUEST_OID,
+ REPL_START_NSDS90_REPLICATION_REQUEST_OID,
+ REPL_NSDS90_REPLICATION_RESPONSE_OID,
+ REPL_CLEANRUV_OID,
+ REPL_ABORT_CLEANRUV_OID,
+ REPL_CLEANRUV_GET_MAXCSN_OID,
+ REPL_CLEANRUV_CHECK_STATUS_OID,
+ REPL_ABORT_SESSION_OID,
+])
+def test_anonymous_extops(topology_m2, init_bind_user, extop_oid):
+ """Test that all replication extended operations fail if anonymous.
+
+ Tests that anonymous (unauthenticated) connections cannot invoke any
+ replication extended operations, ensuring proper authentication is required.
+
+ :id: f6a7b8c9-d0e1-4f5a-2b3c-4d5e6f7a8b9c
+ :parametrized: yes
+ :setup: Replication setup with two suppliers
+ :steps:
+ 1. Create anonymous connection (no bind)
+ 2. Attempt to send extended operation with specified OID
+ 3. Verify operation is rejected
+ :expectedresults:
+ 1. Anonymous connection established
+ 2. Extended operation is sent
+ 3. Operation fails with INSUFFICIENT_ACCESS, SERVER_DOWN, or PROTOCOL_ERROR
+ """
+ supplier1 = topology_m2.ms["supplier1"]
+ filter_bytes = 'foo'
+ payload = create_cleanruv_payload(b'foo')
+ extreq = ldap.extop.ExtendedRequest(extop_oid, payload)
+ expected_exception = ( ldap.INSUFFICIENT_ACCESS, ldap.SERVER_DOWN, ldap.PROTOCOL_ERROR )
+ if extop_oid in (REPL_END_NSDS50_REPLICATION_REQUEST_OID, ):
+ # end session extended operation return success without
+ # doing anything if the replica is not acquired
+ expected_exception = None
+ bind_type = BIND_ANONYMOUS
+ test_description = f'Test {extop_oid} extended operation on anonymous connection'
+ check_extop(supplier1, init_bind_user, extreq, bind_type, filter_bytes, expected_exception, test_description)
+
+
+def test_authorization_revocation(topology_m2, init_bind_user):
+ """Test that removing a user from replication group revokes their authorization.
+
+ This test validates that:
+ - Cached bind DNs work correctly after authentication
+ - Removing a user from the replication group doesn't immediately affect cached credentials
+ - Cache is automatically evicted after timeout
+ - After cache eviction, authorization is re-evaluated and unauthorized users are rejected
+ - Re-adding user to the group restores authorization
+
+ :id: a7b8c9d0-e1f2-4a5b-3c4d-5e6f7a8b9c0d
+ :setup: Replication setup with two suppliers, authorized user in replication group
+ :steps:
+ 1. Verify authorized user can send CleanRUV extended operation (populates cache)
+ 2. Remove user from replication_managers group
+ 3. Wait for bind DN cache eviction (12 seconds)
+ 4. Verify same user now gets INSUFFICIENT_ACCESS (cache miss, re-authorization fails)
+ 5. Re-add user to replication_managers group
+ 6. Wait for group membership cache sync
+ 7. Verify operation works again after re-authorization
+ :expectedresults:
+ 1. Operation succeeds (cache populated with authorized DN)
+ 2. User is removed from group
+ 3. Cache eviction completes
+ 4. Operation fails with INSUFFICIENT_ACCESS or OPERATIONS_ERROR
+ 5. User is re-added to group
+ 6. Group membership synchronized
+ 7. Operation succeeds again (user re-authorized)
+ :cleanup:
+ - User is re-added to group (in finally block)
+ - Group cache is synchronized (in finally block)
+ """
+ supplier1 = topology_m2.ms["supplier1"]
+ supplier2 = topology_m2.ms["supplier2"]
+ groups = Groups(supplier1, basedn=DEFAULT_SUFFIX, rdn=None)
+ repl_group = groups.get(dn=f'cn=replication_managers,{DEFAULT_SUFFIX}')
+ binddn = init_bind_user[BIND_AUTHORIZED][0]
+
+ valid_filter = b"(nsds5ReplicaCleanRUV=5:1234567890:no:0:dc=example,dc=com)"
+ payload = create_cleanruv_payload(valid_filter)
+ extreq = ldap.extop.ExtendedRequest(REPL_CLEANRUV_CHECK_STATUS_OID, payload)
+
+ test_description = 'Check CleanRuv_Check_Status with valid user'
+ expected_exception = None
+
+ try:
+ # Step 1: Verify authorized user can send CleanRUV extended operation
+ log.info(" Step 1: Verify authorized user can send operation (populate cache)")
+ check_extop(supplier1, init_bind_user, extreq, BIND_AUTHORIZED, valid_filter, expected_exception, test_description)
+ log.info(" User DN is now cached")
+
+ # Step 2: Remove user from replication_managers group
+ log.info(" Step 2: Remove user from replication_managers group")
+ repl_group.remove_member(binddn)
+ log.info(f" User {binddn} removed from group")
+
+ # Step 3: Wait enough time to ensure that bind DN cache is flushed
+ log.info(f" Step 3: Wait for group membership cache sync ({REPL_GROUP_SYNC_DELAY} seconds)")
+ time.sleep(REPL_GROUP_SYNC_DELAY)
+ log.info(" Group cache synchronized")
+
+ # Step 4: Verify user now gets INSUFFICIENT_ACCESS
+ log.info(" Step 4: Verify operation now fails (cache miss, re-authorization)")
+ expected_exception = (ldap.OPERATIONS_ERROR, ldap.INSUFFICIENT_ACCESS)
+ check_extop(supplier1, init_bind_user, extreq, BIND_AUTHORIZED, valid_filter, expected_exception, test_description)
+ log.info(" PASS: User correctly rejected after cache eviction")
+
+ finally:
+ # Step 5: Cleanup - Re-add user to group
+ log.info(" Step 5: Re-add user to replication_managers group (cleanup)")
+ repl_group.ensure_member(binddn)
+ log.info(f" User {binddn} re-added to group")
+
+ # Step 6: Wait for group membership cache sync
+ log.info(f" Step 6: Wait for group membership cache sync ({REPL_GROUP_SYNC_DELAY} seconds)")
+ time.sleep(REPL_GROUP_SYNC_DELAY)
+ log.info(" Group cache synchronized")
+
+ # Step 7: Double check that the operation is now working again
+ log.info(" Step 7: Verify operation works again after re-authorization")
+ expected_exception = None
+ check_extop(supplier1, init_bind_user, extreq, BIND_AUTHORIZED, valid_filter, expected_exception, test_description)
+ log.info(" PASS: User successfully re-authorized")
+
+
+if __name__ == '__main__':
+ # Run with: pytest -v test_cleanruv_extop_security.py
+ import sys
+ sys.exit(pytest.main(["-v", os.path.abspath(__file__)]))
diff --git a/ldap/servers/plugins/replication/repl5.h b/ldap/servers/plugins/replication/repl5.h
index 44f8052ed..0bdbf1f6e 100644
--- a/ldap/servers/plugins/replication/repl5.h
+++ b/ldap/servers/plugins/replication/repl5.h
@@ -279,6 +279,7 @@ int multisupplier_extop_cleanruv(Slapi_PBlock *pb);
int multisupplier_extop_abort_cleanruv(Slapi_PBlock *pb);
int multisupplier_extop_cleanruv_get_maxcsn(Slapi_PBlock *pb);
int multisupplier_extop_cleanruv_check_status(Slapi_PBlock *pb);
+bool check_replica_acquired(Slapi_PBlock *pb);
int extop_noop(Slapi_PBlock *pb);
struct berval *NSDS50StartReplicationRequest_new(const char *protocol_oid,
const char *repl_root,
@@ -745,6 +746,7 @@ int replica_update_csngen_state_ext(Replica *r, const RUV *ruv, const CSN *extra
CSN *replica_get_purge_csn(const Replica *r);
int replica_log_ruv_elements(const Replica *r);
void replica_enumerate_replicas(FNEnumReplica fn, void *arg);
+void replica_config_enumerate_replicas(FNEnumReplica fn, void *arg);
int replica_reload_ruv(Replica *r);
int replica_check_for_data_reload(Replica *r, void *arg);
/* the functions below manipulate replica dn hash */
diff --git a/ldap/servers/plugins/replication/repl5_replica_config.c b/ldap/servers/plugins/replication/repl5_replica_config.c
index cfc29d99e..25a7463ef 100644
--- a/ldap/servers/plugins/replication/repl5_replica_config.c
+++ b/ldap/servers/plugins/replication/repl5_replica_config.c
@@ -1297,6 +1297,78 @@ _replica_config_get_mtnode_ext(const Slapi_Entry *e)
return ext;
}
+/* Helper callback to collect replica roots into a charray */
+static int
+replica_collect_root_callback(Replica *r, void *arg)
+{
+ char ***roots = (char ***)arg;
+ const char *root = slapi_sdn_get_dn(replica_get_root(r));
+
+ if (root) {
+ charray_add(roots, slapi_ch_strdup(root));
+ }
+ return 0;
+}
+
+/*
+ * Enumerate replicas without holding locks during callbacks.
+ * This function is slower than replica_enumerate_replicas buf safer
+ * as it prevents deadlocks when callbacks need to acquire other locks.
+ *
+ * Algorithm:
+ * 1. Collect replica roots with hash lock held (brief)
+ * 2. For each root, get mapping tree node
+ * 3. Hold s_configLock briefly to safely access mtnode_ext->replica
+ * 4. Acquire object reference to keep replica alive
+ * 5. Call callback without any locks held
+ * 6. Release object reference
+ */
+void
+replica_config_enumerate_replicas(FNEnumReplica fn, void *arg)
+{
+ char **roots = NULL;
+
+ /* Step 1: Collect replica roots using the hash lock */
+ replica_enumerate_replicas(replica_collect_root_callback, &roots);
+
+ /* Step 2: Iterate without holding the hash lock */
+ if (roots) {
+ for (size_t i = 0; roots[i] != NULL; i++) {
+ Slapi_DN sdn;
+ mapping_tree_node *mtnode;
+ multisupplier_mtnode_extension *mtnode_ext;
+ Object *replica_obj = NULL;
+ Replica *r;
+
+ slapi_sdn_init_dn_byval(&sdn, roots[i]);
+
+ /* CRITICAL: Hold s_configLock when accessing mapping tree and mtnode_ext */
+ PR_Lock(s_configLock);
+ mtnode = slapi_get_mapping_tree_node_by_dn(&sdn);
+ if (mtnode) {
+ mtnode_ext = (multisupplier_mtnode_extension *)repl_con_get_ext(REPL_CON_EXT_MTNODE, mtnode);
+ if (mtnode_ext && mtnode_ext->replica) {
+ replica_obj = mtnode_ext->replica;
+ object_acquire(replica_obj); /* Acquire while lock held */
+ }
+ }
+ PR_Unlock(s_configLock);
+
+ slapi_sdn_done(&sdn);
+
+ if (replica_obj) {
+ r = (Replica *)object_get_data(replica_obj);
+ if (r) {
+ fn(r, arg); /* Call without any locks held */
+ }
+ object_release(replica_obj);
+ replica_obj = NULL;
+ }
+ }
+ charray_free(roots);
+ }
+}
+
/* This thread runs the tests of csn generator.
* It will log a set of csn generated while simulating local and remote time skews
diff --git a/ldap/servers/plugins/replication/repl5_total.c b/ldap/servers/plugins/replication/repl5_total.c
index 25ca66ad2..aae940a86 100644
--- a/ldap/servers/plugins/replication/repl5_total.c
+++ b/ldap/servers/plugins/replication/repl5_total.c
@@ -760,6 +760,19 @@ multisupplier_extop_NSDS50ReplicationEntry(Slapi_PBlock *pb)
slapi_pblock_get(pb, SLAPI_CONN_ID, &connid);
slapi_pblock_get(pb, SLAPI_OPERATION_ID, &opid);
+ /* Check that connection is a replication session */
+ if (!check_replica_acquired(pb)) {
+ slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name,
+ "multisupplier_extop_NSDS50ReplicationEntry - "
+ "Attempt to send entries outside of a replication session conn=%" PRIu64 " op=%d\n",
+ connid, opid);
+ slapi_pblock_get(pb, SLAPI_CONNECTION, &conn);
+ if (conn) {
+ slapi_disconnect_server(conn);
+ }
+ return LDAP_INSUFFICIENT_ACCESS;
+ }
+
/* Decode the extended operation */
rc = decode_total_update_extop(pb, &e);
diff --git a/ldap/servers/plugins/replication/repl_extop.c b/ldap/servers/plugins/replication/repl_extop.c
index 818d99431..53e311621 100644
--- a/ldap/servers/plugins/replication/repl_extop.c
+++ b/ldap/servers/plugins/replication/repl_extop.c
@@ -12,6 +12,7 @@
#endif
+#include <ctype.h>
#include "slapi-plugin.h"
#include "repl5.h"
#include "repl5_prot_private.h"
@@ -45,6 +46,134 @@
*/
static int check_replica_id_uniqueness(Replica *replica, RUV *supplier_ruv);
+typedef struct check_updatedn_data
+{
+ const Slapi_DN *bind_sdn;
+ bool is_updatedn;
+} check_updatedn_data;
+
+/*
+ * Callback function for replica_enumerate_replicas()
+ * Checks if the bind DN in the data structure is an updatedn for the given replica
+ */
+static int
+check_updatedn_callback(Replica *replica, void *arg)
+{
+ check_updatedn_data *data = (check_updatedn_data *)arg;
+
+ if (data->is_updatedn) {
+ /* Already found a match, no need to continue */
+ return 0;
+ }
+ if (replica_is_updatedn(replica, data->bind_sdn)) {
+ data->is_updatedn = true;
+ }
+ return 0;
+}
+
+/*
+ * Check if the connection bind DN is an allowed bind DN for at least one of the replicas
+ *
+ * This function caches the last successful bind DN to avoid repeated replica enumeration
+ * for the same connection. (Useful mostly for bulk import entries)
+ *
+ * Returns:
+ * true - if the bind DN is an updatedn for at least one replica
+ * false - if the bind DN is not an updatedn for any replica, or if connection is anonymous
+ */
+static bool
+check_replica_auth(Slapi_PBlock *pb)
+{
+ char *bind_dn = NULL;
+ Slapi_DN *bind_sdn = NULL;
+ check_updatedn_data data = {0};
+ bool result = false;
+ int isroot = 0;
+
+ slapi_pblock_get(pb, SLAPI_REQUESTOR_ISROOT, &isroot);
+ if (isroot) {
+ return true;
+ }
+
+ /* Get the bind DN from the connection */
+ slapi_pblock_get(pb, SLAPI_CONN_DN, &bind_dn);
+ if (bind_dn == NULL) {
+ /* No bind DN, connection is anonymous */
+ slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name, "check_replica_auth - "
+ "Attempting replication extended operation while anonymous.\n");
+ return false;
+ }
+
+ bind_sdn = slapi_sdn_new_dn_passin(bind_dn);
+ bind_dn = NULL; /* Consumed by slapi_sdn_new_dn_passin */
+ /* Check if the bind DN is an updatedn for any replica */
+ data.bind_sdn = bind_sdn;
+ data.is_updatedn = false;
+ replica_config_enumerate_replicas(check_updatedn_callback, &data);
+ result = data.is_updatedn;
+
+ if (!result) {
+ slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name, "check_replica_auth - "
+ "Invalid binddn %s to perform replication extended operation\n",
+ slapi_sdn_get_dn(bind_sdn));
+ }
+
+ slapi_sdn_free(&bind_sdn);
+
+ return result;
+}
+
+/*
+ * Check if the connection extension exists and has an acquired replica.
+ * This verifies that a replication session is active on this connection.
+ *
+ * Returns:
+ * true - if connection extension exists and has an acquired replica
+ * false - if connection extension doesn't exist or no replica is acquired
+ */
+bool
+check_replica_acquired(Slapi_PBlock *pb)
+{
+ Slapi_Connection *conn = NULL;
+ consumer_connection_extension *connext = NULL;
+ bool result = false;
+
+ slapi_pblock_get(pb, SLAPI_CONNECTION, &conn);
+ if (conn == NULL) {
+ return false;
+ }
+
+ connext = (consumer_connection_extension *)repl_con_get_ext(REPL_CON_EXT_CONN, conn);
+ if (connext != NULL && connext->replica_acquired != NULL) {
+ result = true;
+ }
+
+ return result;
+}
+
+/* Check if filter starts with: (attr=<digit> */
+static inline bool
+check_equality_filter(const char *filter, const char *attr)
+{
+ size_t len = strlen(attr);
+ return (filter[0] == '(' &&
+ strncasecmp(filter+1, attr, len) == 0 &&
+ filter[1+len] == '=' && isdigit((unsigned char)(filter[2+len])));
+}
+
+/* Check that the filter is one of the expected one */
+static bool
+check_valid_filter(const char *filter)
+{
+ if (check_equality_filter(filter, type_replicaAbortCleanRUV)) {
+ return true;
+ }
+ if (check_equality_filter(filter, type_replicaCleanRUV)) {
+ return true;
+ }
+ return false;
+}
+
static int
encode_ruv(BerElement *ber, const RUV *ruv)
{
@@ -695,6 +824,10 @@ multisupplier_extop_StartNSDS50ReplicationRequest(Slapi_PBlock *pb)
struct berval *data = NULL;
int is90 = 0;
+ if (!check_replica_auth(pb)) {
+ slapi_send_ldap_result(pb, LDAP_INSUFFICIENT_ACCESS, NULL, NULL, 0, NULL);
+ return NSDS50_REPL_PERMISSION_DENIED;
+ }
/* Decode the extended operation */
if (decode_startrepl_extop(pb, &protocol_oid, &repl_root, &supplier_ruv,
&referrals, &replicacsnstr, &data_guid, &data, &is90) == -1) {
@@ -1270,6 +1403,17 @@ multisupplier_extop_EndNSDS50ReplicationRequest(Slapi_PBlock *pb)
PRUint64 connid = 0;
int opid = -1;
+ if (!check_replica_acquired(pb)) {
+ /* At this point we do not know the authentication status
+ * If it is a proper end session without acquired replica
+ * there is nothing to do.
+ * So lets just respond NSDS50_REPL_REPLICA_RELEASE_SUCCEEDED
+ * bypassing decoding the payload and avoid associated risk
+ * if it is an attack
+ */
+ response = NSDS50_REPL_REPLICA_RELEASE_SUCCEEDED;
+ goto send_response;
+ }
/* Decode the extended operation */
if (decode_endrepl_extop(pb, &repl_root) == -1) {
response = NSDS50_REPL_DECODING_ERROR;
@@ -1485,6 +1629,10 @@ multisupplier_extop_abort_cleanruv(Slapi_PBlock *pb)
char *iter = NULL;
int rc = LDAP_SUCCESS;
+ if (!check_replica_auth(pb)) {
+ /* Someone is trying something nasty */
+ return LDAP_INSUFFICIENT_ACCESS;
+ }
slapi_pblock_get(pb, SLAPI_EXT_OP_REQ_OID, &extop_oid);
slapi_pblock_get(pb, SLAPI_EXT_OP_REQ_VALUE, &extop_payload);
@@ -1604,6 +1752,10 @@ multisupplier_extop_cleanruv(Slapi_PBlock *pb)
int rid = 0;
int rc = LDAP_OPERATIONS_ERROR;
+ if (!check_replica_auth(pb)) {
+ /* Someone is trying something nasty */
+ return LDAP_INSUFFICIENT_ACCESS;
+ }
slapi_pblock_get(pb, SLAPI_EXT_OP_REQ_OID, &extop_oid);
slapi_pblock_get(pb, SLAPI_EXT_OP_REQ_VALUE, &extop_payload);
@@ -1783,6 +1935,10 @@ multisupplier_extop_cleanruv_get_maxcsn(Slapi_PBlock *pb)
int rid = 0;
int rc = LDAP_OPERATIONS_ERROR;
+ if (!check_replica_auth(pb)) {
+ /* Someone is trying something nasty */
+ return LDAP_INSUFFICIENT_ACCESS;
+ }
slapi_pblock_get(pb, SLAPI_EXT_OP_REQ_OID, &extop_oid);
slapi_pblock_get(pb, SLAPI_EXT_OP_REQ_VALUE, &extop_payload);
@@ -1850,6 +2006,11 @@ multisupplier_extop_cleanruv_check_status(Slapi_PBlock *pb)
int res = 0;
int rc = LDAP_OPERATIONS_ERROR;
+
+ if (!check_replica_auth(pb)) {
+ /* Someone is trying something nasty */
+ return LDAP_INSUFFICIENT_ACCESS;
+ }
slapi_pblock_get(pb, SLAPI_EXT_OP_REQ_OID, &extop_oid);
slapi_pblock_get(pb, SLAPI_EXT_OP_REQ_VALUE, &extop_payload);
@@ -1866,6 +2027,12 @@ multisupplier_extop_cleanruv_check_status(Slapi_PBlock *pb)
"CleanAllRUV Task - Check Status Task: failed to decode payload. Aborting ext op\n");
goto free_and_return;
}
+ if (!check_valid_filter(filter)) {
+ /* Someone is attempting something nasty */
+ slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name, "multisupplier_extop_cleanruv_check_status - "
+ "CleanAllRUV Task - Unexpected filter provided as payload. Aborting ext op\n");
+ goto free_and_return;
+ }
search_pb = slapi_pblock_new();
slapi_search_internal_set_pb(search_pb, "cn=config", LDAP_SCOPE_SUBTREE,
--
2.55.0

View File

@ -0,0 +1,45 @@
From 7d9b7157234e628ecc32baab16a09eef666962d5 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Wed, 3 Jun 2026 17:52:09 -0400
Subject: [PATCH] Issue 7554 - deref plugin null pointer dereference if
ber_init fails
Description:
**CWE**: CWE-476 (NULL Pointer Dereference)
A flaw in the 389 Directory Server's dereference control plugin allows an
unauthenticated attacker to crash the LDAP server when the system is under
memory pressure(OOM). The deref plugin, enabled by default, fails to check for
a memory allocation failure before using the result, causing the server
process to terminate.
CI test 'test_deref_and_access_control' already covers this fix.
relates: https://github.com/389ds/389-ds-base/issues/7554
Reviewed by: tbordaz & progier(Thanks!!)
---
ldap/servers/plugins/deref/deref.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/ldap/servers/plugins/deref/deref.c b/ldap/servers/plugins/deref/deref.c
index fc1c10f71..fc157f6d6 100644
--- a/ldap/servers/plugins/deref/deref.c
+++ b/ldap/servers/plugins/deref/deref.c
@@ -357,6 +357,12 @@ deref_parse_ctrl_value(DerefSpecList *speclist, const struct berval *ctrlbv, int
}
ber = ber_init((struct berval *)ctrlbv);
+ if (!ber) {
+ *ldapcode = LDAP_UNWILLING_TO_PERFORM;
+ *ldaperrtext = "Deref control parsing failed to initialize BER element";
+ return;
+ }
+
for (tag = ber_first_element(ber, &len, &last);
(tag != LBER_ERROR) && (tag != LBER_END_OF_SEQORSET);
tag = ber_next_element(ber, &len, last)) {
--
2.55.0

View File

@ -0,0 +1,41 @@
From 7f7d430dc5ec9285a8d95b69c591ed0225a0f057 Mon Sep 17 00:00:00 2001
From: Thierry Bordaz <tbordaz@redhat.com>
Date: Wed, 15 Jul 2026 11:39:58 +0200
Subject: [PATCH] Issue CVE-2026-15722 - pre-authentication stack buffer
overflow
Bug description:
The function get_ruvelement_from_berval() in ldap/servers/plugins/replication/repl5_ruv.c
parses a replica ID from a Replica Update Vector (RUV) berval by copying digit
characters into a 16-byte stack buffer (ridbuff[RIDSTR_SIZE]). The copy loop
has no bounds check
it keeps writing as long as isdigit() returns true.
A berval with more than 16 digit characters in the replica ID position overflows the buffer.
Fix description:
if the berval contains more than RIDSTR_SIZE-1 digits it fails
fixes: TBD
Reviewed by: Mark Reynolds
---
ldap/servers/plugins/replication/repl5_ruv.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/ldap/servers/plugins/replication/repl5_ruv.c b/ldap/servers/plugins/replication/repl5_ruv.c
index 40869c486..4df6ea135 100644
--- a/ldap/servers/plugins/replication/repl5_ruv.c
+++ b/ldap/servers/plugins/replication/repl5_ruv.c
@@ -1987,6 +1987,9 @@ get_ruvelement_from_berval(const struct berval *bval)
/* replica id must be here */
i = 0;
while (isdigit(bval->bv_val[urlbegin])) {
+ if (i >= RIDSTR_SIZE - 1) {
+ goto loser;
+ }
ridbuff[i] = bval->bv_val[urlbegin];
i++;
urlbegin++;
--
2.55.0

View File

@ -2,7 +2,7 @@
## (rpmautospec version 0.6.5)
## RPMAUTOSPEC: autorelease, autochangelog
%define autorelease(e:s:pb:n) %{?-p:0.}%{lua:
release_number = 8;
release_number = 9;
base_release_number = tonumber(rpm.expand("%{?-b*}%{!?-b:1}"));
print(release_number + base_release_number - 1);
}%{?-e:.%{-e*}}%{?-s:.%{-s*}}%{!?-n:%{?dist}}
@ -390,6 +390,9 @@ Patch: 0081-Issue-7549-Substring-index-should-validate-minimum-n.patc
Patch: 0082-Issue-7593-Reject-invalid-SASL-packet-length-values-.patch
Patch: 0083-Issue-7593-Fix-testimony-docstring-for-SASL-overflow.patch
Patch: 0084-Security-Advisory-Heap-Buffer-Overflow-in-sasl_io_re.patch
Patch: 0085-CVE-2026-11770-pre-auth-LDAP-filter-injection-in-Cle.patch
Patch: 0086-Issue-7554-deref-plugin-null-pointer-dereference-if-.patch
Patch: 0087-Issue-CVE-2026-15722-pre-authentication-stack-buffer.patch
%description
389 Directory Server is an LDAPv3 compliant server. The base package includes
@ -942,6 +945,16 @@ exit 0
%changelog
## START: Generated by rpmautospec
* Wed Jul 29 2026 Viktor Ashirov <vashirov@redhat.com> - 3.2.0-9
- Bump version to 3.2.0-9
- Resolves: RHEL-183075 - CVE-2026-11770 389-ds-base: 389-ds-base: pre-auth
LDAP filter injection in CleanAllRUV status check [rhel-10.2.z]
- Resolves: RHEL-190776 - CVE-2026-11788 389-ds-base: 389-ds-base: NULL
pointer dereference in deref control plugin BER parser [rhel-10.2.z]
- Resolves: RHEL-210874 - CVE-2026-15722 389-ds-base: 389-ds-base: pre-
authentication stack buffer overflow in get_ruvelement_from_berval() via
unbounded replica ID parsing [rhel-10.2.z]
* Fri Jun 26 2026 Viktor Ashirov <vashirov@redhat.com> - 3.2.0-8
- Bump version to 3.2.0-8
- Resolves: RHEL-182152 - CVE-2026-11610 389-ds-base: 389-ds-base: Heap