GHSA-p279-2cqp-84jg SASL PLAIN authzid bypassing the proxy ACI scope check
| | |
| | | * |
| | | * Copyright 2006-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyrighted 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.extensions; |
| | | |
| | |
| | | return; |
| | | } |
| | | } |
| | | |
| | | if (! checkProxyAccess(bindOperation, userEntry, authZEntry)) |
| | | { |
| | | return; |
| | | } |
| | | } |
| | | } |
| | | else |
| | |
| | | bindOperation.setAuthFailureReason(message); |
| | | return; |
| | | } |
| | | |
| | | if (! checkProxyAccess(bindOperation, userEntry, authZEntry)) |
| | | { |
| | | return; |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | 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) |
| | | { |
| | |
| | | * |
| | | * Copyright 2008-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyrighted 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.extensions; |
| | | |
| | |
| | | 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; |
| | | |
| | | /** |
| | |
| | | */ |
| | | 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); |
| | | } |
| | | |
| | | |
| | |
| | | "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", |
| | |
| | | "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\";)"); |
| | | |
| | |
| | | |
| | | |
| | | /** |
| | | * 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. |