安全审查 - PHP 的 password_hash 实现

信息安全 密码 哈希 php 代码审查 C
2021-08-26 13:39:21

我目前正在为 PHP 的核心开发一个“帮助函数”,以使密码散列对大多数开发人员来说更安全、更容易。基本上,目标是让它变得如此简单,以至于发明自己的实现比使用核心安全实现更难。随着散列方法的改进,它还被设计为在未来进行更新和扩展。

我已经为 add 编写了一个RFC,并且还 实现了这些功能

基本前提是crypt对于广大开发者来说直接正确使用太难了。奇怪的错误返回,类似 base64(但使用不同的字母)的盐等。所以这些函数旨在消除这种猜测,并提供一个非常简单的 API。

string password_hash(string $password, string $algo = PASSWORD_DEFAULT, array $options = array())
bool password_verify(string $password, string $hash)
string password_make_salt(int $length, bool $raw_output = false)

Password_Hash 接受密码、可选算法说明符(目前仅CRYPT_BCRYPT支持改进的实现,但希望scrypt稍后作为选项添加)和选项数组。options 数组可以指定costbcrypt 的参数,以及预定义的 salt 值。

Password_Verify 接受密码和现有哈希。然后它重新散列密码(与 相同$tmp = crypt($password, $hash))。然后,它使用恒定时间比较函数来确定两个哈希是否确实相等。

Password_Make_Salt 的存在是为了生成给定长度的随机字符串。如果raw_output设置为 false(默认),则输出的“salt”将以与crypt(). 如果raw_output为真,它将使用随机全字节 ( 0-255) 返回相同长度的字符串。

例子:

$hash = password_hash("foo");
if (password_verify("foo", $hash)) {
    // always should be true
} else {
}

阅读 PHP 源代码的注意事项:PHP 函数(暴露于 php 代码的函数)被PHP_FUNCTION()宏包围。此外,php 变量(zval's)在一些地方被使用。访问其中一部分的宏是

  • Z_TYPE_P() (查找指向 zval 的指针的类型)
  • Z_STRVAL_P() (获取指向字符串值的指针)
  • Z_STRLEN_P()(获取int字符串类型的长度)
  • Z_LVAL_P()(获取long整数类型的值)

此外,zval_ptr_dtor()是一种 refcount 机制,用于减少 a 上的 refcount zval,并在它命中时将其清除0

在正式提出更改之前,我正在寻找至少一些安全专家对实施的审查。它相当短(只有大约 300 行代码)......

更新

API 已获得批准,因此我为该功能设置了拉取请求。如果您有时间,请查看它。谢谢!: https ://github.com/php/php-src/pull/191

3个回答

在 C 中,我通常建议您应该始终使用size_t来存储所有长度值,而不是intor long使用整数有符号/无符号错误int并将其long暴露给您。参考:来自CERT C 安全编码标准的INT01-C

但是,由于 PHP 原生代码接口的工作方式,在这种情况下会有一个复杂的情况:

  • 当您使用 读取整数时zend_parse_parameters(),PHP 希望您将其存储在long. 因此,您应该传递zend_parse_parameters()一个指向 a 的指针long,然后小心地将 a 转换long为 a size_t:验证该数字是非负数并且在size_t( l >= 0 && l < SIZE_MAX) 的范围内,然后将其转换为 asize_t并在之后使用 a size_t

  • 当您使用 读取字符串时zend_parse_parameters(),PHP 期望您传递一个缓冲区来存储字符串,并传递一个指向 的指针来int存储字符串长度。因此,您应该通过zend_parse_parameters()它所期望的。然后,小心地将 转换int为 a :验证长度是否为非负数并且在( )size_t的范围内,然后将其转换为 a并在其后使用 a size_tlen >= 0 && len < SIZE_MAXsize_tsize_t

    同样,Z_STRLEN_PP()返回一个int(我认为),所以你可能想为它做同样的事情。

(注意将 along作为长度传递给memcpy();我不确定,但该代码对我来说非常可疑,并且存在整数转换/截断错误的高风险。)

buffer = php_base64_encode((unsigned char*) str, str_len, NULL);
for (pos = 0; pos < out_len; pos++) {
    if (buffer[pos] == '+') {
         ...
    } else {
         ret[pos] = buffer[pos];

看起来它可以读到buffer我的结尾。我认为您需要进行额外检查以确保它pos小于buffer.

非常好的倡议!


快速评论
(稍后我将更详细地阅读 RFC。)

“盐只需要在系统中是唯一的”是一个错误的假设;另请阅读:这个关于盐渍的答案

我看不出需要为 BCrypt 算法手动提供盐。据我所知,它只是为人们提供了一个选择,让他们犯下不必要的错误。

password_make_salt 似乎与 crypt_ 系列函数更相关;也许将其重命名为 crypt_make_salt,以避免混淆。

第一个基本用法示例提倡使用常量值“usesomesillystringfor”,许多开发人员会错误地使用某种常量值而不是适当的(随机)盐。

password_make_salt 应该使用尽可能好的随机源,如果可能的话,加密安全(我不确定 php_win32_get_random_bytes() 的质量,/dev/urandom 很好)

user_needs_rehash.php 示例可读性不强;可以使用清理。

specify_salt.php 示例建议您可以提供随机字节作为 BCrypt 算法的有效盐值。这与 BCrypt 的自定义 base64 字母表的要求相矛盾。

从散列的角度来看,使用 Bcrypt 是当今最推荐的技术之一,所以它很好。

让用户选择使用另一种算法(例如 Scrypt,也许他们以后会自己实现?)也是一个不错的选择:默认情况下(对于非专业 PHP 开发人员),他们只需要使用默认的 BCrypt ,如果他们愿意,他们将能够适应他们的需求。