import Oracle_OSS 389-ds-base-3.2.0-9.el10_2
This commit is contained in:
parent
7241c1017b
commit
af701dcc1a
874
0085-CVE-2026-11770-pre-auth-LDAP-filter-injection-in-Cle.patch
Normal file
874
0085-CVE-2026-11770-pre-auth-LDAP-filter-injection-in-Cle.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user