密码包含&符号时,php ldap_bind失败(&)

时间:2012-10-12 11:52:37

标签: php escaping

我的这个函数在我的活动目录下进行身份验证,它的工作正常,除了包含像这样的特殊字符的一些密码:   Xefeéà&"+  总是返回'无效凭据'

我在互联网上看过几个帖子,但似乎都没有正确地逃过这些帖子。

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

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

ldap_set_option($this->_link, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($this->_link, LDAP_OPT_REFERRALS, 0); 

登录功能:

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);
}

非常感谢任何帮助。

2 个答案:

答案 0 :(得分:5)

我们发现自己处于类似的状态,我们的企业环境LDAP认证设置对于我们的一个内部托管的Web应用程序似乎工作得很好,直到用户的密码中有特殊字符 - 字符如? &安培; *&#39; - 想要使用该应用程序,然后很明显,某些东西实际上没有按预期工作。

我们发现,当受影响的用户中出现特殊字符时,我们的身份验证请求在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无法理解的字符,即:&#34; _&#34;   和&#34; $&#34;

一系列修复措施已经过广泛测试,包括清洁&#39;密码通过html_entity_decode()utf8_encode()等,并确保Web应用程序和各种服务器之间没有密码错误编码,但无论做什么调整或保护输入文本以确保其原始UTF-8编码保持不变,ldap_bind()在密码中出现一个或多个特殊字符时始终失败(我们没有测试用户名中包含特殊字符的案例,但其他人已报告用户名包含特殊字符而不是密码时的类似问题。

我们还使用命令行上运行的最小PHP脚本直接测试ldap_bind(),其中包含密码中包含特殊字符的测试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);

很明显,仅使用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);

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

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

最后,值得注意的是,LDAP协议要求使用UTF-8根据RFC(RFC 4511, p16, first paragraph)对密码进行编码 - 因此,如果有请求期间使用任何系统的情况不会将用户的凭据作为UTF-8从初始条目一直处理到身份验证,这也可能是值得研究的区域。

希望这个解决方案对于那些努力用许多其他报告的解决方案解决这个问题的人来说是有用的,但仍然发现无法对用户进行身份验证的情况。可能需要针对您自己的LDAP环境调整代码,尤其是ldap_search()中使用的DN,因为这取决于您的LDAP目录的配置方式以及您为应用程序支持的用户组。很高兴,它在我们的环境中运作良好,并希望该解决方案在其他地方证明是有用的。

答案 1 :(得分:1)

尝试使用html_entity_decode代码而不是utf8_decode;这是唯一对我有用的东西。