CVE-2020-35518: Observable Discrepancy (CWE-203)
value: When binding against a DN during authentication, the reply from 389-ds-base will be different whether the DN exists or not. This can be used by an unauthenticated attacker to check the existence of an entry in the LDAP database.
CWE description: The product behaves differently or sends different responses under different circumstances in a way that is observable to an unauthorized actor, which exposes security-relevant information about the state of the product, such as whether a particular operation was successful or not.
CVSS v3 base score: 5.3
Published: 2021-03-26T17:15Z
Repository: https://github.com/389ds/389-ds-base
Language: C
Files changed: dirsrvtests/tests/suites/basic/basic_test.py, ldap/servers/slapd/back-ldbm/ldbm_bind.c, ldap/servers/slapd/dse.c
Commit date: 2021-02-10 09:29:31 -0500
Commit message: Issue 4609 - CVE - info disclosure when authenticating

Description:  If you bind as a user that does not exist.  Error 49 is returned
              instead of error 32.  As error 32 discloses that the entry does
              not exist.  When you bind as an entry that does not have userpassword
              set then error 48 (inappropriate auth) is returned, but this
              discloses that the entry does indeed exist.  Instead we should
              always return error 49, even if the password is not set in the
              entry.  This way we do not disclose to an attacker if the Bind
              DN exists or not.

Relates: https://github.com/389ds/389-ds-base/issues/4609

Reviewed by: tbordaz(Thanks!)

Diff: diff --git a/dirsrvtests/tests/suites/basic/basic_test.py b/dirsrvtests/tests/suites/basic/basic_test.py
index 3c4c9a79d..5a7b75ef1 100644
--- a/dirsrvtests/tests/suites/basic/basic_test.py
+++ b/dirsrvtests/tests/suites/basic/basic_test.py
@@ -7,11 +7,11 @@
 # --- END COPYRIGHT BLOCK ---
 #
 
 from subprocess import check_output, PIPE, run
 from lib389 import DirSrv
-from lib389.idm.user import UserAccounts
+from lib389.idm.user import UserAccount, UserAccounts
 import pytest
 from lib389.tasks import *
 from lib389.utils import *
 from lib389.topologies import topology_st
 from lib389.dbgen import dbgen_users
@@ -1061,22 +1061,18 @@ def test_search_ou(topology_st):
 
 def test_bind_invalid_entry(topology_st):
     """Test the failing bind does not return information about the entry
 
     :id: 5cd9b083-eea6-426b-84ca-83c26fc49a6f
-
     :customerscenario: True
-
     :setup: Standalone instance
-
     :steps:
-    1: bind as non existing entry
-    2: check that bind info does not report 'No such entry'
-
+        1: bind as non existing entry
+        2: check that bind info does not report 'No such entry'
     :expectedresults:
-    1: pass
-    2: pass
+        1: pass
+        2: pass
     """
 
     topology_st.standalone.restart()
     INVALID_ENTRY="cn=foooo,%s" % DEFAULT_SUFFIX
     try:
@@ -1094,10 +1090,47 @@ def test_bind_invalid_entry(topology_st):
 
     # reset credentials
     topology_st.standalone.simple_bind_s(DN_DM, PW_DM)
 
 
+def test_bind_entry_missing_passwd(topology_st):
+    """
+    :id: af209149-8fb8-48cb-93ea-3e82dd7119d2
+    :setup: Standalone Instance
+    :steps:
+        1. Bind as database entry that does not have userpassword set
+        2. Bind as database entry that does not exist
+        1. Bind as cn=config entry that does not have userpassword set
+        2. Bind as cn=config entry that does not exist
+    :expectedresults:
+        1. Fails with error 49
+        2. Fails with error 49
+        3. Fails with error 49
+        4. Fails with error 49
+    """
+    user = UserAccount(topology_st.standalone, DEFAULT_SUFFIX)
+    with pytest.raises(ldap.INVALID_CREDENTIALS):
+        # Bind as the suffix root entry which does not have a userpassword
+        user.bind("some_password")
+
+    user = UserAccount(topology_st.standalone, "cn=not here," + DEFAULT_SUFFIX)
+    with pytest.raises(ldap.INVALID_CREDENTIALS):
+        # Bind as the entry which does not exist
+        user.bind("some_password")
+
+    # Test cn=config since it has its own code path
+    user = UserAccount(topology_st.standalone, "cn=config")
+    with pytest.raises(ldap.INVALID_CREDENTIALS):
+        # Bind as the config entry which does not have a userpassword
+        user.bind("some_password")
+
+    user = UserAccount(topology_st.standalone, "cn=does not exist,cn=config")
+    with pytest.raises(ldap.INVALID_CREDENTIALS):
+        # Bind as an entry under cn=config that does not exist
+        user.bind("some_password")
+
+
 @pytest.mark.bz1044135
 @pytest.mark.ds47319
 def test_connection_buffer_size(topology_st):
     """Test connection buffer size adjustable with different values(valid values and invalid)
 
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_bind.c b/ldap/servers/slapd/back-ldbm/ldbm_bind.c
index fa450ecd5..38d115a32 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_bind.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_bind.c
@@ -74,12 +74,12 @@ ldbm_back_bind(Slapi_PBlock *pb)
 
     switch (method) {
     case LDAP_AUTH_SIMPLE: {
         Slapi_Value cv;
         if (slapi_entry_attr_find(e->ep_entry, "userpassword", &attr) != 0) {
-            slapi_send_ldap_result(pb, LDAP_INAPPROPRIATE_AUTH, NULL,
-                                   NULL, 0, NULL);
+            slapi_pblock_set(pb, SLAPI_PB_RESULT_TEXT, "Entry does not have userpassword set");
+            slapi_send_ldap_result(pb, LDAP_INVALID_CREDENTIALS, NULL, NULL, 0, NULL);
             CACHE_RETURN(&inst->inst_cache, &e);
             rc = SLAPI_BIND_FAIL;
             goto bail;
         }
         bvals = attr_get_present_values(attr);
diff --git a/ldap/servers/slapd/dse.c b/ldap/servers/slapd/dse.c
index f5572d78d..f2741aeb4 100644
--- a/ldap/servers/slapd/dse.c
+++ b/ldap/servers/slapd/dse.c
@@ -1444,26 +1444,29 @@ dse_bind(Slapi_PBlock *pb) /* JCM There should only be one exit point from this
         return (SLAPI_BIND_FAIL);
     }
 
     ec = dse_get_entry_copy(pdse, sdn, DSE_USE_LOCK);
     if (ec == NULL) {
-        slapi_send_ldap_result(pb, LDAP_NO_SUCH_OBJECT, NULL, NULL, 0, NULL);
+        slapi_pblock_set(pb, SLAPI_PB_RESULT_TEXT, "Entry does not exist");
+        slapi_send_ldap_result(pb, LDAP_INVALID_CREDENTIALS, NULL, NULL, 0, NULL);
         return (SLAPI_BIND_FAIL);
     }
 
     switch (method) {
     case LDAP_AUTH_SIMPLE: {
         Slapi_Value cv;
         if (slapi_entry_attr_find(ec, "userpassword", &attr) != 0) {
-            slapi_send_ldap_result(pb, LDAP_INAPPROPRIATE_AUTH, NULL, NULL, 0, NULL);
+            slapi_pblock_set(pb, SLAPI_PB_RESULT_TEXT, "Entry does not have userpassword set");
+            slapi_send_ldap_result(pb, LDAP_INVALID_CREDENTIALS, NULL, NULL, 0, NULL);
             slapi_entry_free(ec);
             return SLAPI_BIND_FAIL;
         }
         bvals = attr_get_present_values(attr);
 
         slapi_value_init_berval(&cv, cred);
         if (slapi_pw_find_sv(bvals, &cv) != 0) {
+            slapi_pblock_set(pb, SLAPI_PB_RESULT_TEXT, "Invalid credentials");
             slapi_send_ldap_result(pb, LDAP_INVALID_CREDENTIALS, NULL, NULL, 0, NULL);
             slapi_entry_free(ec);
             value_done(&cv);
             return SLAPI_BIND_FAIL;
         }

Vulnerable code: from lib389.idm.user import UserAccounts



    1: bind as non existing entry
    2: check that bind info does not report 'No such entry'

    1: pass
    2: pass
            slapi_send_ldap_result(pb, LDAP_INAPPROPRIATE_AUTH, NULL,
                                   NULL, 0, NULL);
        slapi_send_ldap_result(pb, LDAP_NO_SUCH_OBJECT, NULL, NULL, 0, NULL);
            slapi_send_ldap_result(pb, LDAP_INAPPROPRIATE_AUTH, NULL, NULL, 0, NULL);
Fixed code: from lib389.idm.user import UserAccount, UserAccounts
        1: bind as non existing entry
        2: check that bind info does not report 'No such entry'
        1: pass
        2: pass
def test_bind_entry_missing_passwd(topology_st):
    """
    :id: af209149-8fb8-48cb-93ea-3e82dd7119d2
    :setup: Standalone Instance
    :steps:
        1. Bind as database entry that does not have userpassword set
        2. Bind as database entry that does not exist
        1. Bind as cn=config entry that does not have userpassword set
        2. Bind as cn=config entry that does not exist
    :expectedresults:
        1. Fails with error 49
        2. Fails with error 49
        3. Fails with error 49
        4. Fails with error 49
    """
    user = UserAccount(topology_st.standalone, DEFAULT_SUFFIX)
    with pytest.raises(ldap.INVALID_CREDENTIALS):
        # Bind as the suffix root entry which does not have a userpassword
        user.bind("some_password")

    user = UserAccount(topology_st.standalone, "cn=not here," + DEFAULT_SUFFIX)
    with pytest.raises(ldap.INVALID_CREDENTIALS):
        # Bind as the entry which does not exist
        user.bind("some_password")

    # Test cn=config since it has its own code path
    user = UserAccount(topology_st.standalone, "cn=config")
    with pytest.raises(ldap.INVALID_CREDENTIALS):
        # Bind as the config entry which does not have a userpassword
        user.bind("some_password")

    user = UserAccount(topology_st.standalone, "cn=does not exist,cn=config")
    with pytest.raises(ldap.INVALID_CREDENTIALS):
        # Bind as an entry under cn=config that does not exist
        user.bind("some_password")


            slapi_pblock_set(pb, SLAPI_PB_RESULT_TEXT, "Entry does not have userpassword set");
            slapi_send_ldap_result(pb, LDAP_INVALID_CREDENTIALS, NULL, NULL, 0, NULL);
        slapi_pblock_set(pb, SLAPI_PB_RESULT_TEXT, "Entry does not exist");
        slapi_send_ldap_result(pb, LDAP_INVALID_CREDENTIALS, NULL, NULL, 0, NULL);
            slapi_pblock_set(pb, SLAPI_PB_RESULT_TEXT, "Entry does not have userpassword set");
            slapi_send_ldap_result(pb, LDAP_INVALID_CREDENTIALS, NULL, NULL, 0, NULL);
            slapi_pblock_set(pb, SLAPI_PB_RESULT_TEXT, "Invalid credentials");