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
| Package | Version | Status |
|---|---|---|
| 389-ds-base (upstream) | All versions (1.x through 3.2.0-dev HEAD) | Vulnerable (confirmed) |
| 389-ds-base (Fedora 42) | 3.1.4 | Vulnerable (confirmed, tested) |
| 389-ds-base (RHEL 9) | 2.6.x | Vulnerable (confirmed) |
| 389-ds-base (RHEL 10) | 3.1.x | Vulnerable (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
| Product | Version | Status |
|---|---|---|
| 389 Directory Server (upstream) | All | Affected — ldap_utf8prev() present since initial Mozilla LDAP C SDK import; unchanged in git history |
| RHEL 9 | 9.0-9.6 | Affected — 389-ds-base 2.0.x-2.6.x |
| RHEL 10 | 10.0-10.1 | Affected — 389-ds-base 3.0.x-3.1.x |
| Fedora | All | Affected — Tested: 389-ds-base-3.1.4-6.fc42. No crash on production binary. |
| Red Hat Directory Server 12 | 12.0-12.7 | Affected — Ships same 389-ds-base as RHEL 9 AppStream |
| Red Hat Directory Server 13 | 13.0-13.1 | Affected — Ships same 389-ds-base as RHEL 10 |
| Red Hat IdM / FreeIPA | All | Affected — Uses 389-ds-base as the LDAP backend |
| CentOS Stream | 9, 10 | Affected — 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): Addedptr >= startguards to 8 call sites inacllas.c,aclparse.c, andvalue.c. These guards prevent using the OOB-derived pointer but do not prevent the OOB read insideldap_utf8prev(). - 2020 (commit
13f8dc7b6, Issue #4107 / BZ#1758478): Hardened ACI input validation inaclparse.cto reject double-equals syntax before reachingstr2simple(). Did NOT fixstr2simple()orldap_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:
=at offset 0 (minimal trigger — reads 1+ bytes before buffer)- UTF-8 continuation bytes before
=(forces deeper backward scan) - Maximum backward scan with 5 continuation bytes (reads 6 bytes before buffer)
- 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
| Component | Value | Justification |
|---|---|---|
| AV (Attack Vector) | Network | Triggered via LDAP protocol (ports 389/636), though indirectly via internal callers processing network-derived data |
| AC (Attack Complexity) | High | Attacker 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) | Low | Minimum authenticated access required to influence plugin behavior or attribute values that feed into internal filter construction |
| UI (User Interaction) | None | No user interaction required |
| S (Scope) | Unchanged | Impact limited to the LDAP server process |
| C (Confidentiality) | Low | Reads 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) | Low | OOB 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) | Low | No 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:
| Dependent | Relationship | Impact |
|---|---|---|
| Red Hat Identity Management (IdM/FreeIPA) | Uses 389-ds-base as its LDAP directory | Filter misparse could affect IdM user/group/host lookups, HBAC rules, sudo rules |
| Red Hat Directory Server 12/13 | IS 389-ds-base (rebranded + supported) | Directly affected |
| Red Hat Certificate System (Dogtag PKI) | Uses 389-ds-base for certificate and key storage | Filter misparse could affect certificate lookup/issuance |
| Red Hat Satellite | Uses IdM/FreeIPA for authentication backend | Indirectly affected through IdM dependency |
| SSSD | Queries 389-ds-base for user/group resolution | Could receive incorrect results from misparsed filters |
Applications using slapi_str2filter() API | Direct callers of the vulnerable function | Any 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 (commit13f8dc7b6) 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_utf8prevOOB reads. No exploitation reported. - NVD/CVE database — No prior CVE assigned for
ldap_utf8prevOOB 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_8incollate.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
| Date | Event |
|---|---|
| 2026-04-14 | Discovered during 389-ds-base security assessment |
| 2026-04-15 | Reported to vendor |
References
- Red Hat CVE page
- NVD
- GitHub Issue #4107 — Prior ASan report of the same bug (2020, incomplete fix)
- BZ#339791 — 2007 segfault fix for
ldap_utf8prevOOB reads - BZ#1758478 — Tracking bug for Issue #4107
- CWE-125: Out-of-bounds Read
- CVE-2018-1054 — Related 389-ds-base OOB read
- CVE-2024-1062 — Related 389-ds-base heap overflow
Credits
Discovered by Ian Murphy