当密码包含“&”号时,php ldap_bind失败

Loï*_*HEL 3 php escaping

我有这个功能可以在我的活动目录中进行身份验证,它的工作正常,除了一些包含特殊字符的密码,例如:: Xefeéà&"+ 总是返回“无效的凭证”

我在互联网上看过几篇文章,但似乎都没有正确地避开字符。

我正在使用PHP版本5.3.8-ZS(Zend服务器)

这是我的类构造函数中的设置:

ldap_set_option($this->_link, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($this->_link, LDAP_OPT_REFERRALS, 0); 
Run Code Online (Sandbox Code Playgroud)

和登录功能:

public function login($userlogin,$userpassword){
$return=array();
$userlogin.="@".$this->_config["domaine"];
$ret=@ldap_bind($this->_link , $userlogin ,utf8_decode($userpassword));
if(!$ret){
    $return[]=array("status"=>"error","message"=>"Erreur d'authentification ! <br/> Veuillez vérifier votre nom et mot de passe SVP","ldap_error"=>ldap_error($this->_link));
}
if($ret)
{
    $return[]=array("status"=>"success","message"=>"Authentification réussie");
}
return json_encode($return);
}
Run Code Online (Sandbox Code Playgroud)

任何帮助将不胜感激。

blu*_*ary 5

我们发现自己处于类似的解决方案中,其中一个内部托管的Web应用程序的企业环境LDAP身份验证设置似乎运行良好,直到用户的密码中包含特殊字符(如?)为止。&*'-想使用该应用程序,然后很明显,某些东西实际上没有按预期工作。


我们发现,ldap_bind()在受影响的用户的密码中出现特殊字符时,我们的身份验证请求失败,通常会看到诸如Invalid credentialsNDS error: failed authentication (-669)(从LDAP的扩展错误日志记录)输出到日志中的错误。实际上,正是NDS error: failed authentication (-669)错误导致发现绑定失败的原因可能是密码中存在特殊字符-通过此Novell支持文章https://www.novell.com/support/kb/doc.php? id = 3335671,最引人注意的部分摘录如下:

管理员密码包含LDAP无法识别的字符,即:“ _”和“ $”

已经对广泛的修复程序进行了广泛的测试,包括通过html_entity_decode()utf8_encode()等“清除”密码,并确保在Web应用程序和各种服务器之间不会发生密码的误编码,但是无论如何进行调整或修改都不会。保护输入文本以确保其原始UTF-8编码保持完好,ldap_bind()当密码中包含一个或多个特殊字符时总是会失败(我们没有测试用户名中包含特殊字符的案例,但是当用户输入用户名包含特殊字符,而不是密码或除密码之外)。

我们还ldap_bind()使用在命令行上运行的最小PHP脚本(使用硬编码的用户名和密码)直接测试了测试LDAP帐户,该帐户在密码中包含特殊字符(包括问号,星号和感叹号),以尝试消除尽可能多的潜在失败原因,但是绑定操作仍然失败。因此,很明显,Web应用程序和服务器之间的密码错误编码并不是问题,所有这些密码均设计和构建为符合UTF-8端到端。相反,似乎ldap_bind()在处理特殊字符方面存在潜在的问题。同一测试脚本成功绑定了另一个密码中没有任何特殊字符的帐户,进一步证实了我们的怀疑。

早期版本的LDAP身份验证的工作方式如下,大多数PHP LDAP身份验证教程建议:

$success = false; // could we authenticate the user?

if(!defined("LDAP_OPT_DIAGNOSTIC_MESSAGE")) {
    define("LDAP_OPT_DIAGNOSTIC_MESSAGE", 0x0032); // needed for more detailed logging
}

if(($ldap = ldap_connect($ldap_server)) !== false && is_resource($ldap)) {
    ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
    ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);

    if(@ldap_bind($ldap, sprintf("cn=%s,ou=Workers,o=Internal", $username), $password)) {
        $success = true; // login succeeded...
    } else {
        error_log(sprintf("* Unable to authenticate user (%s) against LDAP!", $username));
        error_log(sprintf(" * LDAP Error: %s", ldap_error($ldap)));

        if(ldap_get_option($ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extended_error)) {
            error_log(sprintf(" * LDAP Extended Error: %s\n", $extended_error));
            unset($extended_error);
        }
    }
}
@ldap_close($ldap); unset($ldap);
Run Code Online (Sandbox Code Playgroud)

显然,我们的更简单的解决方案仅使用ldap_connect()并且ldap_bind()不允许具有复杂密码的用户进行身份验证,因此我们不得不寻找一种替代方法。

我们选择了一个基于以下解决方案的解决方案:首先使用系统帐户(具有必要的特权来搜索用户,但有意将其配置为受限的只读帐户)来绑定到LDAP服务器。然后,我们使用来搜索具有提供的用户名的用户帐户ldap_search(),然后ldap_compare()将用户提供的密码与LDAP中存储的密码进行比较。事实证明,该解决方案是可靠且有效的,并且我们现在可以对用户使用密码中的特殊字符进行身份验证,对于没有密码的用户也是如此!

新的LDAP流程的工作方式如下:

if(!defined("LDAP_OPT_DIAGNOSTIC_MESSAGE")) {
    define("LDAP_OPT_DIAGNOSTIC_MESSAGE", 0x0032); // needed for more detailed logging
}

$success = false; // could we authenticate the user?

if(($ldap = @ldap_connect($ldap_server)) !== false && is_resource($ldap)) {
    ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
    ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);

    // instead of trying to bind the user, we bind to the server...
    if(@ldap_bind($ldap, $ldap_username, $ldap_password)) {
        // then we search for the given user...
        if(($search = ldap_search($ldap, "ou=Workers,o=Internal", sprintf("(&(uid=%s))", $username))) !== false && is_resource($search)) {
            // they should be the first and only user found for the search...
            if((ldap_count_entries($ldap, $search) == 1) && ($entry = ldap_first_entry($ldap, $search)) !== false && is_resource($entry)) {
                // we ensure this is the case by obtaining the user identifier (UID) from the search which must match the provided $username...
                if(($uid = ldap_get_values($ldap, $entry, "uid")) !== false && is_array($uid)) {
                    // ensure that just one entry was returned by ldap_get_values() and ensure the obtained value matches the provided $username (excluding any case-differences)
                    if((isset($uid["count"]) && $uid["count"] == 1) && (isset($uid[0]) && is_string($uid[0]) && (strcmp(mb_strtolower($uid[0], "UTF-8"), mb_strtolower($username, "UTF-8")) === 0))) {
                        // once we have compared the provied $username with the discovered username, we get the DN for the user, which we need to provide to ldap_compare()...
                        if(($dn = ldap_get_dn($ldap, $entry)) !== false && is_string($dn)) {
                            // we then use ldap_compare() to compare the user's password with the provided $password
                            // ldap_compare() will respond with a boolean true/false depending on if the comparison succeeded or not, and -1 on error...
                            if(($comparison = ldap_compare($ldap, $dn, "userPassword", $password)) !== -1 && is_bool($comparison)) {
                                if($comparison === true) {
                                    $success = true; // user successfully authenticated, if we don't get this far, it failed...
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    if(!$success) { // if not successful, either an error occurred or the credentials were actually incorrect
        error_log(sprintf("* Unable to authenticate user (%s) against LDAP!", $username));
        error_log(sprintf(" * LDAP Error: %s", ldap_error($ldap)));
        if(ldap_get_option($ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extended_error)) {
            error_log(sprintf(" * LDAP Extended Error: %s\n", $extended_error));
            unset($extended_error);
        }
    }
}
@ldap_close($ldap);
unset($ldap);
Run Code Online (Sandbox Code Playgroud)

最初的担忧之一是,这种带有所有额外步骤的新方法执行起来将花费更长的时间,但事实证明(至少在我们的环境中)ldap_bind()使用ldap_search()和运行简单解决方案与更复杂解决方案之间的时间差异ldap_compare()是实际上微不足道,平均可能要长0.1秒。

请注意,我们的LDAP身份验证代码的早期版本和最新版本均应在验证并测试了用户输入数据(即用户名和密码输入)之后才能运行,以确保没有提供空的或无效的用户名或密码字符串,并且没有无效字符(例如空字节)存在。此外,理想情况下,应该通过来自应用程序的安全HTTPS请求发送用户凭据,并且可以使用安全LDAPS协议在身份验证过程中进一步保护用户凭据。我们发现直接使用LDAPS,通过ldaps://...而不是通过连接到服务器ldap://...,然后再切换到TLS连接,已被证明是更可靠的。如果您的设置有所不同,则需要对上述代码进行相应的调整,以纳入对ldap_start_tls()

最后,可能值得注意的是,LDAP协议要求根据RFC(RFC 4511,p16,第一段)使用UTF-8对密码进行编码-因此,在某些情况下,请求期间使用的任何系统都是从最初的输入到身份验证,一直没有将用户凭据作为UTF-8进行处理,这也可能是一个值得研究的领域。

希望该解决方案将对其他使用许多其他已报告的解决方案努力解决此问题但仍发现无法通过身份验证的用户提供帮助。可能需要针对您自己的LDAP环境调整代码,尤其是所用的DN,ldap_search()因为这将取决于如何配置LDAP目录以及您为应用程序支持的用户组。很高兴,它在我们的环境中运行良好,希望该解决方案在其他地方也有用。