php ldap_bind在密码中包含&符号时失败

bn31dyow  于 2023-10-15  发布在  PHP
关注(0)|答案(4)|浏览(126)

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

任何帮助将不胜感激。

thtygnil

thtygnil1#

我们发现自己处于类似的情况,我们的企业环境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.1s。
需要注意的是,我们的LDAP身份验证代码的早期版本和后期版本都应该只在用户输入数据(即,用户名和密码输入)已被验证和测试,以确保没有提供空或无效的用户名或密码字符串,并且不存在无效字符(如空字节)。此外,理想情况下,用户凭据应该通过应用程序的安全HTTPS请求发送,并且可以使用安全LDAPS协议在身份验证过程中进一步保护用户凭据。我们发现直接使用LDAPS,通过ldaps://...连接到服务器,而不是通过ldap://...连接,然后切换到TLS连接,已经证明更可靠。如果您的设置不同,则需要相应地调整上述代码以包含对ldap_start_tls()的支持。
最后,值得注意的是,LDAP协议要求密码按照RFC(RFC 4511, p16, first paragraph)使用UTF-8编码-因此,如果在请求期间使用的任何系统从初始输入到身份验证都没有将用户的凭据作为UTF-8处理,这也是值得研究的领域。

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

ifmq2ha2

ifmq2ha22#

尝试html_entity_decode作为密码,而不是utf8_decode;这是唯一对我有用的方法

yvgpqqbh

yvgpqqbh3#

使用ldap_compare时要格外小心!它的编写无处不在,不使用它的二进制值,因为在某些环境中,例如AD:它总是返回true
https://www.php.net/manual/en/function.ldap-compare.php

警告ldap_compare()不能用于比较BINARY值!

qrjkbowd

qrjkbowd4#

您可能需要使用LDAP v3

ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);

LDAPv 3默认支持UTF-8,它期望请求和响应默认使用UTF-8。
这解决了我的问题与密码有“€”或“@”或“#”

相关问题