// Moderate (3.7)
CVE-2026-11787

Heap buffer over-read in ldap_utf8prev() via str2simple filter parsing

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

Summary

The ldap_utf8prev() function in 389 Directory Server unconditionally reads up to 6 bytes before a heap allocation because it lacks a lower-bound parameter. This API design flaw has been known since 2007 and was symptomatically patched twice (2007, 2020) without fixing the root cause. Twenty call sites across the codebase remain vulnerable to 1-6 byte heap over-reads. The bug cannot be directly triggered via the LDAP wire protocol, but internal callers that process attacker-influenced data (plugin configuration, ACI definitions, replication) are affected. No crash occurs on production binaries; the over-read silently influences filter parsing behavior.

Affected Versions

PackageVersionStatus
389-ds-base (upstream)All versions (1.x through 3.2.0-dev HEAD)Vulnerable (confirmed)
389-ds-base (Fedora 42)3.1.4Vulnerable (confirmed, tested)
389-ds-base (RHEL 9)2.6.xVulnerable (confirmed)
389-ds-base (RHEL 10)3.1.xVulnerable (confirmed)

Note: ldap_utf8prev() has been present unchanged since the project’s first commit (inherited from the Mozilla LDAP C SDK). No version of 389-ds-base has ever included a lower-bound check in this function.

Affected Products

ProductVersionStatus
389 Directory Server (upstream)AllAffected — ldap_utf8prev() present since initial Mozilla LDAP C SDK import; unchanged in git history
RHEL 99.0-9.6Affected — 389-ds-base 2.0.x-2.6.x
RHEL 1010.0-10.1Affected — 389-ds-base 3.0.x-3.1.x
FedoraAllAffected — Tested: 389-ds-base-3.1.4-6.fc42. No crash on production binary.
Red Hat Directory Server 1212.0-12.7Affected — Ships same 389-ds-base as RHEL 9 AppStream
Red Hat Directory Server 1313.0-13.1Affected — Ships same 389-ds-base as RHEL 10
Red Hat IdM / FreeIPAAllAffected — Uses 389-ds-base as the LDAP backend
CentOS Stream9, 10Affected — Matches RHEL

Root Cause

File: ldap/servers/slapd/utf8.c:104-113 Function: ldap_utf8prev() Trigger call site: ldap/servers/slapd/str2filter.c:321 (str2simple() -> LDAP_UTF8DEC(s)) Verified against: 389-ds-base upstream HEAD commit 411276274b7 (2026-04-22)

The ldap_utf8prev() function scans backward through UTF-8 continuation bytes to find the start of the previous character. It takes a single char *s parameter but has no lower-bound parameter to indicate the start of the buffer:

/* utf8.c, lines 104-113 */
char *
ldap_utf8prev(char *s)
{
    register unsigned char *prev = (unsigned char *)s;
    unsigned char *limit = prev - 6;
    while (((*--prev & 0xC0) == 0x80) && (prev != limit)) {
        ;
    }
    return (char *)prev;
}

The *--prev expression decrements the pointer before reading the byte. When s points at or within 6 bytes of the start of a heap allocation, the function reads 1-6 bytes before the allocation boundary. The limit = prev - 6 clamp prevents scanning more than 6 bytes backward but does not prevent reading before the buffer.

In str2simple(), the trigger is:

/* str2filter.c, lines 317-321 */
if ((s = strchr(str, '=')) == NULL) {
    return (NULL);
}
value = s;
LDAP_UTF8INC(value);
LDAP_UTF8DEC(s);        /* <-- THE BUG: no check that s > str */

When = appears at position 0 of the filter string, LDAP_UTF8DEC(s) (which expands to ldap_utf8prev(s)) reads before the heap allocation.

History of incomplete fixes:

  • 2007 (commit ba6ce7958, BZ#339791): Added ptr >= start guards to 8 call sites in acllas.c, aclparse.c, and value.c. These guards prevent using the OOB-derived pointer but do not prevent the OOB read inside ldap_utf8prev().
  • 2020 (commit 13f8dc7b6, Issue #4107 / BZ#1758478): Hardened ACI input validation in aclparse.c to reject double-equals syntax before reaching str2simple(). Did NOT fix str2simple() or ldap_utf8prev(). Other trigger paths remain.

20 call sites remain vulnerable across str2filter.c, attrsyntax.c, pw.c, plugins/syntaxes/value.c, plugins/acl/acllas.c, plugins/acl/aclparse.c, and plugins/collation/orfilter.c.

Proposed Fix

Option A — Fix at the call site in str2simple() (minimal change):

/* str2filter.c, line ~317-321 */
if ((s = strchr(str, '=')) == NULL) {
    return (NULL);
}
value = s;
LDAP_UTF8INC(value);
if (s > str) {         /* ADD: bounds check before backward scan */
    LDAP_UTF8DEC(s);
} else {
    /* = at start of filter, no operator prefix possible */
    /* fall through to default (equality) case */
}

Option B — Fix in ldap_utf8prev() itself (recommended, eliminates entire bug class):

/* utf8.c — replace ldap_utf8prev() */
char *
ldap_utf8prev(char *s, char *start)  /* ADD: start-of-buffer parameter */
{
    if (s <= start) return (char *)start;
    register unsigned char *prev = (unsigned char *)s;
    unsigned char *limit = prev - 6;
    if (limit < (unsigned char *)start)
        limit = (unsigned char *)start;
    while (prev > (unsigned char *)start && ((*--prev & 0xC0) == 0x80) && (prev != limit)) {
        ;
    }
    return (char *)prev;
}

Option B requires updating all callers of ldap_utf8prev() and the LDAP_UTF8DEC macro, but eliminates the entire bug class at the root. The 2007 and 2020 fixes demonstrate that per-caller patches are incomplete — the 2007 fix guarded 8 sites but left 12 others unguarded.

Proof of Concept

PoC source code: CVE-2026-11787 on GitHub

Four PoC files are provided in the poc/ directory:

poc/016-str2simple-utf8prev-oob-read.c — ASan reproduction

Standalone C program that replicates the exact ldap_utf8prev() logic from utf8.c and demonstrates the heap-buffer-overflow read when = appears at or near position 0 of a heap-allocated filter string. Four test cases:

  1. = at offset 0 (minimal trigger — reads 1+ bytes before buffer)
  2. UTF-8 continuation bytes before = (forces deeper backward scan)
  3. Maximum backward scan with 5 continuation bytes (reads 6 bytes before buffer)
  4. Control case with valid ASCII before = (no OOB read)

Usage:

gcc -fsanitize=address -g -o poc_016 poc/016-str2simple-utf8prev-oob-read.c
./poc_016

ASan reports heap-buffer-overflow at ldap_utf8prev for cases 1-3. Case 4 (control) produces no error.

poc/016-str2simple-network-test.py — Production binary gate test

Sends malformed LDAP search filters via ldapsearch to a running 389 Directory Server instance. Confirms that malformed filters where = appears at position 0 are rejected client-side (LDAP_FILTER_ERROR) and never reach the server. Verifies server stability after all tests.

poc/016-raw-ldap-test.py — Raw BER protocol test

Sends raw BER-encoded LDAP SearchRequests with malformed filters directly to the LDAP port. Confirms that the server handles all malformed BER filters gracefully with no crash. Demonstrates that wire-protocol filters use BER encoding (RFC 4511 Section 4.5.1), which is parsed by get_filter() in filter.c, not by the vulnerable str2simple() path.

poc/016-orfilter-oob-test.py — Collation plugin variant test

Tests whether the collation plugin’s substring matching can trigger the OOB read when stored attribute values contain UTF-8 continuation bytes. Confirms that DirectoryString syntax validation rejects bare continuation bytes and that the collation substring matching path is not exploitable through this vector.

Impact

ComponentValueJustification
AV (Attack Vector)NetworkTriggered via LDAP protocol (ports 389/636), though indirectly via internal callers processing network-derived data
AC (Attack Complexity)HighAttacker must influence a string passed to slapi_str2filter() with = near position 0 — requires specific plugin/config code path, not a direct wire-protocol attack. However, 20 vulnerable call sites increase the probability that at least one path is reachable.
PR (Privileges Required)LowMinimum authenticated access required to influence plugin behavior or attribute values that feed into internal filter construction
UI (User Interaction)NoneNo user interaction required
S (Scope)UnchangedImpact limited to the LDAP server process
C (Confidentiality)LowReads 1-6 bytes of adjacent heap data. OOB byte value influences filter operator selection (3-way branch), providing ~1.6 bits of information per invocation. Repeated queries could theoretically reconstruct adjacent heap content.
I (Integrity)LowOOB byte can silently change filter semantics (equality vs GE/LE/APPROX), causing incorrect search results. In combination with ACIs that use targetfilter, this could return entries the client should not see (or vice versa).
A (Availability)LowNo crash on production binary (confirmed). However, persistent filter misparse behavior could cause service-level data integrity issues in directory-dependent applications.

Severity rationale: This finding is assessed as Moderate because: (1) the root cause affects 20 call sites, not just one — the API itself is unsafe; (2) two prior fix attempts (2007, 2020) failed to address the root cause, demonstrating that per-caller patching is insufficient; (3) the integrity impact from silent filter misparse can affect ACL enforcement; (4) new callers of ldap_utf8prev() will be vulnerable by default.

Blast Radius

389-ds-base is the LDAP backend for multiple products and services:

DependentRelationshipImpact
Red Hat Identity Management (IdM/FreeIPA)Uses 389-ds-base as its LDAP directoryFilter misparse could affect IdM user/group/host lookups, HBAC rules, sudo rules
Red Hat Directory Server 12/13IS 389-ds-base (rebranded + supported)Directly affected
Red Hat Certificate System (Dogtag PKI)Uses 389-ds-base for certificate and key storageFilter misparse could affect certificate lookup/issuance
Red Hat SatelliteUses IdM/FreeIPA for authentication backendIndirectly affected through IdM dependency
SSSDQueries 389-ds-base for user/group resolutionCould receive incorrect results from misparsed filters
Applications using slapi_str2filter() APIDirect callers of the vulnerable functionAny plugin or application that constructs filter strings from variable data is a potential trigger

Exploitation in the Wild

No evidence of exploitation found.

Searches performed:

  • GitHub 389-ds-base Issue #4107 — Documents the exact same ASan trace (ldap_utf8prev <- str2simple), filed 2020-04-27 by mreynolds. Fix (commit 13f8dc7b6) hardened ACI input validation but did not fix the root cause. No exploitation reported.
  • BZ#1758478 — Tracking bug for Issue #4107. No exploitation reported.
  • BZ#339791 — 2007 segfault fix for ldap_utf8prev OOB reads. No exploitation reported.
  • NVD/CVE database — No prior CVE assigned for ldap_utf8prev OOB reads specifically.
  • Exploit-DB, Snyk, OpenCVE — No matching entries.
  • CISA KEV catalog — No 389-ds-base entries.

Comparable CVEs

  • CVE-2018-1054 (389-ds-base, OOB memory read in SetUnicodeStringFromUTF_8 in collate.c): CVSS 7.5 High — pre-auth, direct wire-protocol crash. Higher severity because it was unauthenticated and caused DoS.
  • CVE-2024-1062 (389-ds-base, heap-based buffer overflow in log_entry_attr): CVSS 5.5 Medium — authenticated, DoS. Similar severity class because it requires authentication.
  • CVE-2025-14905 (389-ds-base, heap buffer overflow, RCE+DoS): Rated Moderate. Higher impact due to write primitive.

Timeline

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

References

Credits

Discovered by Ian Murphy