mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valera V Harseko
23 hours ago 5c326850f1ab945cfca7ac9c5aaf77d1052c6bed
GHSA-p279-2cqp-84jg SASL PLAIN authzid bypassing the proxy ACI scope check
3 files modified
266 ■■■■■ changed files
opendj-server-legacy/src/main/java/org/opends/server/extensions/PlainSASLMechanismHandler.java 42 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/extensions/SASLContext.java 56 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/types/PrivilegeTestCase.java 168 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/extensions/PlainSASLMechanismHandler.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2009 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyrighted 2026 3A Systems, LLC.
 */
package org.opends.server.extensions;
@@ -330,6 +331,11 @@
              return;
            }
          }
          if (! checkProxyAccess(bindOperation, userEntry, authZEntry))
          {
            return;
          }
        }
      }
      else
@@ -391,6 +397,11 @@
            bindOperation.setAuthFailureReason(message);
            return;
          }
          if (! checkProxyAccess(bindOperation, userEntry, authZEntry))
          {
            return;
          }
        }
      }
    }
@@ -444,6 +455,37 @@
    return;
  }
  /**
   * Checks whether the authenticated user is allowed by the access control
   * subsystem to assume the given authorization identity (i.e. holds the
   * "proxy" access control right for the target entry), and if not sets the
   * bind failure result. This delegates to {@link SASLContext#hasProxyAccess}
   * so that SASL PLAIN enforces the same ACI scope check as the proxied
   * authorization controls and the DIGEST-MD5 / GSSAPI {@code authzid}, in
   * addition to the {@code PROXIED_AUTH} privilege check. The denial uses the
   * same {@code INVALID_CREDENTIALS} result and message as DIGEST-MD5 / GSSAPI
   * so a client cannot distinguish a missing privilege from a missing proxy
   * grant.
   *
   * @param  bindOperation  The bind operation being processed.
   * @param  authEntry      The authenticated user entry (the proxy user).
   * @param  authZEntry     The entry to be assumed, or {@code null} for the
   *                        anonymous / root authorization identity.
   * @return  {@code true} if the access control configuration permits the
   *          authenticated user to assume the authorization identity.
   */
  private boolean checkProxyAccess(BindOperation bindOperation, Entry authEntry, Entry authZEntry)
  {
    if (! SASLContext.hasProxyAccess(authEntry, authZEntry, bindOperation))
    {
      bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
      bindOperation.setAuthFailureReason(
          ERR_SASL_AUTHZID_INSUFFICIENT_ACCESS.get(authEntry.getName()));
      return false;
    }
    return true;
  }
  @Override
  public boolean isPasswordBased(String mechanism)
  {
opendj-server-legacy/src/main/java/org/opends/server/extensions/SASLContext.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008-2009 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyrighted 2026 3A Systems, LLC.
 */
package org.opends.server.extensions;
@@ -58,6 +59,7 @@
import org.opends.server.types.AuthenticationInfo;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.Operation;
import org.opends.server.types.Privilege;
/**
@@ -927,30 +929,54 @@
   */
  private boolean hasPermission(final AuthenticationInfo authInfo)
  {
    boolean ret = true;
    Entry e = authzEntry;
    // If the authz entry is null, use the entry associated with the NULL DN.
    if (e == null)
    if (!hasProxyAccess(authInfo.getAuthenticationEntry(), authzEntry, bindOp))
    {
      setCallbackMsg(ERR_SASL_AUTHZID_INSUFFICIENT_ACCESS.get(authEntry.getName()));
      return false;
    }
    return true;
  }
  /**
   * Determines whether the access control configuration permits the
   * authenticated user to assume the given authorization identity, i.e. holds
   * the "proxy" access control right for the target entry. A {@code null}
   * authorization entry maps to the entry associated with the NULL DN (the
   * anonymous / root authorization identity). This is the shared SASL
   * proxy-scope check used by DIGEST-MD5 / GSSAPI and by SASL PLAIN.
   *
   * @param authEntry
   *          The authenticated user entry (the proxy user).
   * @param authzEntry
   *          The entry to be assumed, or {@code null} for the anonymous / root
   *          authorization identity.
   * @param operation
   *          The operation being processed.
   * @return {@code true} if the authenticated user may assume the authorization
   *         identity.
   */
  static boolean hasProxyAccess(final Entry authEntry, final Entry authzEntry,
      final Operation operation)
  {
    Entry proxiedEntry = authzEntry;
    if (proxiedEntry == null)
    {
      // A null authorization entry maps to the entry associated with the NULL DN.
      try
      {
        e = DirectoryServer.getEntry(DN.rootDN());
        proxiedEntry = DirectoryServer.getEntry(DN.rootDN());
      }
      catch (final DirectoryException ex)
      catch (final DirectoryException e)
      {
        logger.traceException(e);
        return false;
      }
    }
    if (!AccessControlConfigManager.getInstance().getAccessControlHandler()
        .mayProxy(authInfo.getAuthenticationEntry(), e, bindOp))
    {
      setCallbackMsg(ERR_SASL_AUTHZID_INSUFFICIENT_ACCESS.get(authEntry.getName()));
      ret = false;
    }
    return ret;
    return AccessControlConfigManager.getInstance().getAccessControlHandler()
        .mayProxy(authEntry, proxiedEntry, operation);
  }
opendj-server-legacy/src/test/java/org/opends/server/types/PrivilegeTestCase.java
@@ -218,7 +218,31 @@
      "givenName: PWReset",
      "sn: Target",
      "uid: pwreset.target",
      "userPassword: password");
      "userPassword: password",
      "",
      "dn: cn=Proxy Only User,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "cn: Proxy Only User",
      "givenName: Proxy",
      "sn: User",
      "uid: proxy.only",
      "userPassword: password",
      "ds-privilege-name: proxied-auth",
      "",
      "dn: cn=Proxy ACI User,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "cn: Proxy ACI User",
      "givenName: Proxy",
      "sn: ACI User",
      "uid: proxy.aci",
      "userPassword: password",
      "ds-privilege-name: proxied-auth");
    TestCaseUtils.applyModifications(false,
      "dn: o=test",
@@ -230,6 +254,8 @@
           "userdn=\"ldap:///cn=Unprivileged Root,cn=Root DNs,cn=config\";)",
      "aci: (version 3.0; acl \"Privileged User\"; allow (proxy) " +
           "userdn=\"ldap:///cn=Privileged User,o=test\";)",
      "aci: (version 3.0; acl \"Proxy ACI User\"; allow (proxy) " +
           "userdn=\"ldap:///cn=Proxy ACI User,o=test\";)",
      "aci: (targetattr=\"*\")(version 3.0; acl \"PWReset Target\"; " +
           "allow (all) userdn=\"ldap:///cn=PWReset Target,o=test\";)");
@@ -2146,6 +2172,146 @@
  /**
   * Regression test for GHSA-p279-2cqp-84jg: an authentication identity that
   * holds the PROXIED_AUTH privilege but is not granted the "proxy" access
   * control right for the target must be denied when specifying an alternate
   * authorization ID through SASL PLAIN using the "dn:" syntax, matching the
   * behaviour of the proxied authorization control and DIGEST-MD5.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testPLAINDifferentAuthzIDNoProxyAciDeniedDNColon()
         throws Exception
  {
    // Control: the proxy user can authenticate and act as itself, proving its
    // entry and password are valid so the denial below is an authorization
    // failure and not a setup or authentication problem.
    String[] selfArgs =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=dn:cn=Proxy Only User,o=test",
      "-o", "authzid=dn:cn=Proxy Only User,o=test",
      "-w", "password",
      "-b", "",
      "-s", "base",
      "(objectClass=*)"
    };
    assertEquals(runSearchWithSystemErr(selfArgs), 0);
    // Holding PROXIED_AUTH but lacking a "proxy" ACI over the target must be
    // denied with INVALID_CREDENTIALS, matching DIGEST-MD5 / GSSAPI.
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=dn:cn=Proxy Only User,o=test",
      "-o", "authzid=dn:cn=Unprivileged User,o=test",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };
    assertEquals(runSearch(args), INVALID_CREDENTIALS.intValue());
  }
  /**
   * Regression test for GHSA-p279-2cqp-84jg: an authentication identity that
   * holds the PROXIED_AUTH privilege but is not granted the "proxy" access
   * control right for the target must be denied when specifying an alternate
   * authorization ID through SASL PLAIN using the "u:" syntax, matching the
   * behaviour of the proxied authorization control and DIGEST-MD5.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testPLAINDifferentAuthzIDNoProxyAciDeniedUColon()
         throws Exception
  {
    // Control: the proxy user can authenticate and act as itself, proving its
    // entry and password are valid so the denial below is an authorization
    // failure and not a setup or authentication problem.
    String[] selfArgs =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=u:proxy.only",
      "-o", "authzid=u:proxy.only",
      "-w", "password",
      "-b", "",
      "-s", "base",
      "(objectClass=*)"
    };
    assertEquals(runSearchWithSystemErr(selfArgs), 0);
    // Holding PROXIED_AUTH but lacking a "proxy" ACI over the target must be
    // denied with INVALID_CREDENTIALS, matching DIGEST-MD5 / GSSAPI.
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=u:proxy.only",
      "-o", "authzid=u:unprivileged.user",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };
    assertEquals(runSearch(args), INVALID_CREDENTIALS.intValue());
  }
  /**
   * Regression test for GHSA-p279-2cqp-84jg: an authentication identity that
   * holds the PROXIED_AUTH privilege AND is granted the "proxy" access control
   * right for the target through an ACI, but does NOT hold the bypass-acl
   * privilege, must be allowed to assume an alternate authorization ID through
   * SASL PLAIN. This confirms the added mayProxy check does not over-deny
   * legitimate ACI-granted proxying (the bypass-acl proxy users exercised by
   * the other success tests short-circuit the ACI evaluation).
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testPLAINDifferentAuthzIDWithProxyAciSucceedsDNColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=dn:cn=Proxy ACI User,o=test",
      "-o", "authzid=dn:cn=Unprivileged User,o=test",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };
    assertEquals(runSearchWithSystemErr(args), 0);
  }
  /**
   * Tests the ability to disable a privilege so that an operation which will
   * fail for a user without an appropriate privilege will succeed if that
   * privilege is disabled.