我的PHP登录系统有多安全?

40

我是 PHP 的新手,这也是我的第一个登录系统,如果你们能看一下我的代码并找出任何安全漏洞那就太好了:

注意:尽管此处未显示,但我正在对所有用户输入进行消毒处理。

注册:

步骤1:我将用户选择的密码传递给以下函数:

encrypt($user_chosen_password, $salt);

function encrypt($plain_text, $salt) {
    if(!$salt) {
        $salt = uniqid(rand(0, 1000000));
    }
    return array(
        'hash' => $salt.hash('sha512', $salt.$plain_text),
        'salt' => $salt
    );
}

步骤2:我会将散列值和盐值($password['hash']$password['salt'])存储在数据库中的用户表中:

id | username | password  | salt       | unrelated info...
-----------------------------------------------------------
1  | bobby    | 809a28377 | 809a28377f | ...
                fd131e5934
                180dc24e15
                bbe5f8be77
                371623ce36
                4d5b851e46

登录:

步骤1: 我获取用户输入的用户名并在数据库中查找是否有任何行返回。在我的网站上,没有2个用户可以共享相同的用户名,因此用户名字段始终具有唯一的值。如果我返回了1行,我会获取该用户的salt。

步骤2: 然后我运行用户输入的密码通过加密函数(如上面先前发布的)但这次我还提供从数据库检索到的salt:

encrypt($user_entered_password, $salt);

步骤3:现在我有了要与之匹配的正确密码,保存在变量$password['hash']中。因此,在数据库上进行第二次查找以查看输入的用户名和哈希密码是否同时返回一行。如果是,则用户凭据正确。

步骤4:为了在用户凭据通过后登录用户,我生成一个随机唯一字符串并对其进行哈希处理:

$random_string = uniqid(rand(0, 1000000));
$session_key = hash('sha512', $random_string);

然后我将$session_key插入到数据库中的active_sessions表中:

user_id | key
------------------------------------------------------------
1       | 431b5f80879068b304db1880d8b1fa7805c63dde5d3dd05a5b

步骤 5:

我将上一步生成的未加密唯一字符串 ($random_string) 设置为一个名为 active_session 的 cookie 的值:

setcookie('active_session', $random_string, time()+3600*48, '/');

步骤6:

在我的header.php文件的顶部,有这个检查:

if(isset($_COOKIE['active_session']) && !isset($_SESSION['userinfo'])) {
   get_userinfo();
}

get_userinfo()函数在数据库的users表上进行查找,并返回一个关联数组,该数组存储在名为userinfo的会话中。

// 首先,此函数获取活动会话cookie的值并对其进行哈希以获取会话密钥:

hash('sha512', $random_string);

// 然后它在active_sessions表上进行查找,以查看是否存在一个由此key相关的记录,如果是,则将获取与该记录关联的user_id并使用其进行第二次查找users表来获取userinfo

    $_SESSION['userinfo'] = array(
        'user_id'           => $row->user_id,
        'username'          => $row->username,
        'dob'               => $row->dob,
        'country'           => $row->country,
        'city'              => $row->city,
        'zip'               => $row->zip,
        'email'             => $row->email,
        'avatar'            => $row->avatar,
        'account_status'    => $row->account_status,
        'timestamp'         => $row->timestamp,
    ); 
如果存在userinfo会话,我知道用户已通过身份验证。如果不存在该会话但存在active_session cookie,则在header.php文件顶部的检查将创建该会话。
我之所以使用cookie而不仅仅是会话,是为了保持登录状态。因此,如果用户关闭浏览器,则会话可能消失,但cookie仍然存在。由于在header.php的顶部有检查,因此会话将被重新创建,用户可以像正常登录用户一样运行。
登出:
第1步:取消设置userinfo会话和active_session cookie。
第2步:从数据库中删除active_sessions表中的相关记录。
注意:唯一的问题是,如果用户伪造active_session cookie并在其浏览器中自行创建它,则可能存在其他问题(也许还有很多)。当然,他们必须将该cookie的值设置为一个字符串,该字符串在加密后必须与来自active_sessions表中的记录匹配,我将从该记录中检索user_id以创建该会话。我不确定实际上,对于用户(可能使用自动程序)来说,猜测一个他们不知道将被sha512加密后与数据库中的字符串匹配以获取用户ID以构建该会话的字符串的准确机会是多少。
抱歉写了这么大一篇文章,但由于这是我网站的如此关键部分,并且由于我的经验不足,我只是想请更有经验的开发人员进行审核,以确保它实际上是安全的。
那么,您是否发现了这种方法中的任何安全漏洞,并且该如何改进?

3
显而易见的是,如果您没有使用HTTPS来传输用户名/密码,那么您很容易受到多种攻击(编辑:除此之外,我个人看不出任何漏洞。但这只是图片的一小部分。复杂性要求等也很重要,还要检查所有网站代码路径)。 - Kieren Johnstone
CA签名证书的成本并不是那么昂贵(大约100美元,也有一些名义上免费的来源)。只要您了解需要做什么(包括在https上提供所有内容,包括图像),就会更好。另请参见:http://security.stackexchange.com/ - Jared Farrish
3
考虑到整体情况,这只是一个小问题,但建议使用 mt_rand() 而不是 rand() - Grexis
1
请注意,哈希加密是完全不同的:使用正确的密钥可以解密加密文本,而哈希文本则无法被反向转换(单向函数)。 - Gumbo
CA签名证书每个证书的价格可以低至每年9-10美元。我个人使用ssls.com,自2011年起就从他们那里获得我的证书(当时他们还叫cheapssls.com)。 - Doktor J
显示剩余7条评论
5个回答

29
你应该包含某种超时或故障转移以防止暴力攻击。有许多方法可以做到这一点,包括基于IP的阻止、增量超时等。这些都永远不会完全"停止"黑客,但它们可以使攻击变得更加困难。
另一个点(你没有提到,所以我不知道你的计划)是失败消息。使失败消息尽可能模糊。 提供错误信息,例如 "该用户名存在,但密码不匹配" 可能对最终用户有所帮助,但它会破坏登录功能。你刚刚将应该花费 O(n^2) 时间的暴力攻击转换为了 O(n) + O(n) 。黑客只需要先尝试所有用户名(使用一组密码),直到失败消息发生变化。然后,它"知道"一个有效用户,并只需暴力攻击密码。
同样,在用户名存在和不存在的情况下,确保经过相同的时间。当用户名实际存在时,你正在运行其他进程。因此,响应时间将比不存在时长。一个非常熟练的黑客可以计时页面请求来查找有效的用户名。
类似地,除了过期 cookie 外,还要确保过期会话表。
最后,在get_user_info()调用中,如果存在多个并发的活动登录,则应终止所有打开的会话。确保在一定时间内无活动后超时会话(例如30分钟)。
就像@Greg Hewgill所提到的那样,你没有包括以下任何内容:
SSL/加密连接服务器-客户端
您可能正在使用其他传输协议来处理身份验证(例如OAuth)
你的服务器是安全的,但如果有人可以读取交换的数据(MITM),则算法的安全性再强也无济于事。确保只通过加密协议进行通信。

是的,我计划引入x次登录失败尝试超时。至少这样可以延迟自动化机器人进行暴力攻击的尝试。 - user967451
2
我讨厌网站告诉我“密码与电子邮件地址不匹配”,然后当我尝试一个无意义的电子邮件时,它说“用户不存在”。还有...当网站给我发送我“忘记”的密码时。我看到一些所谓的风险敏感的网站这样做,但他们拒绝承认为什么这样做是有问题的。 - Jared Farrish

9

运行用户输入的密码通过加密函数...

那么密码是如何从浏览器传输到服务器的呢?您还没有提到如何防止中间人攻击。


你是指有人在用户的网络上监听,就像需要 SSL 一样吗? - user967451
2
是的,没错。如果我能够看到浏览器和服务器之间的数据包(有很多方法),那么我就能够看到用户的密码以明文的方式传输。 - Greg Hewgill

7

这段代码...

function encrypt($plain_text, $salt) {
    if(!$salt) {
        $salt = uniqid(rand(0, 1000000));
    }
    return array(
        'hash' => $salt.hash('sha512', $salt.$plain_text),
        'salt' => $salt
    );
}

使用旧的密码API是不好的。使用新的密码API,这样就完成了。除非你是专家,否则不应该尝试设计自己的认证系统。它们极难实现正确

在用户通过验证后登录,我会生成一个随机唯一字符串并对其进行哈希:

只需让PHP处理会话管理。rand() 和 mt_rand() 都是非常不安全的随机数生成器。


3

你编写的代码似乎无法通过自动化单元测试和集成测试进行测试。这使得难以及时发现可能包含在生产环境中的实现错误。

通常,这会导致安全问题,因为严格和正确执行以及合理的数据处理的安全性未经过测试/验证。

(这只是列表上的另一点,请参见有关如何保护传输层的答案,另外您还没有说明如何防止会话数据被篡改。)


这是一个有趣的答案。您是否有任何其他链接或信息,可以帮助解释如何解决这个问题? - Jared Farrish
我来举个例子:如果 Paula 在数据库中有相同的 pw-hash 和 hash,她可能能够获取 Bobby 的身份。通过抽象存储进行单元测试,很容易通过边界测试发现这样的问题,例如重复值。这可能是在实现过程中被忽视的细节,因为希望使一切正确。这些情况最好早点发现。这只是标准的测试内容,所以你应该可以在网上找到很多相关主题的信息。除此之外,我建议访问 http://nostarch.com/codecraft.htm。 - hakre
我认为对于单元测试和集成测试的好处感兴趣的大多数人都是那些可能没有实践经验来看到实际回报的一次性和小团队。我在想,“数据库中的相同pw-hashhash”是什么意思?这是一个碰撞测试吗? - Jared Farrish
密码和盐。这是一个测试,如果它们在行之间发生冲突会发生什么,是的。我不理解你写的关于团队的部分。 - hakre
如果你关心应用程序中的一个单元是否能够正常工作,那么你需要进行测试。仅仅进行代码审查是不够的。我并不是说你必须要使用TDD,但是你需要进行测试。即使对于这样的组件,你也需要尝试去破坏它。 - hakre
那是样板文件。我不确定你是否完全理解我所提出的上下文。 "正常工作" 是 "它对最终用户的工作频率" 的函数还是 "它对[同一]最终用户的问题频率" 的商数?有时我们并不在完全预先指定或测试的环境中工作。TDD很棒,但当“失败”的风险不是“彻底失败”时,最坏的情况也是最好的情况,即使开发人员(或开发人员)都不想要这两种情况。 - Jared Farrish

2

关于php中的密码,你不应该使用加密来处理它们。你应该使用password_hash()对其进行哈希处理,然后在登录时,使用password_verify()来验证html表单中输入的密码是否与数据库中存储的哈希值匹配。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接