繁体   English   中英

php ldap_bind 当密码包含与号 (&) 时失败

[英]php ldap_bind fails when password contains ampersand (&)

我有这个功能用于在我的活动目录上进行身份验证,它工作正常,除了一些包含特殊字符的密码,例如: 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); 

和登录功能:

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

任何帮助将不胜感激。

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


我们发现当受影响的用户密码中存在特殊字符时,我们的身份验证请求在调用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()总是失败(我们没有测试用户名中包含特殊字符的情况) ,但是当用户名包含特殊字符而不是密码时,其他人报告了类似的问题。

我们还使用命令行上运行的最小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,第16页,第一段 )编码密码 - 因此,如果在某些情况下,请求期间使用的任何系统都是不会将用户的凭据作为UTF-8从初始条目一直到身份验证,这也可能是一个值得研究的领域。

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

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

ldap_compare非常小心! 到处都写着不将它用于二进制值,因为在某些环境中,例如 AD:它始终返回true

https://www.php.net/manual/en/function.ldap-compare.php

警告ldap_compare()不能用于比较二进制值!

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM