PHP 8.2 新特性 — 敏感参数值脱敏支持
像其他编程语言一样,PHP 支持在程序的任意点追踪调用的栈。在调试时,栈跟踪很常用,因为他允许追溯调用的函数和方法。
栈跟踪的每一帧都包含函数名和参数。获取栈帧不会中断程序,在不中断程序的情况下静默地捕获栈帧及日志有很多用途。
异常抛出时,异常对象也包含了导致异常抛出的异常点栈帧信息。另外,PHP 提供了内置 debug_backtrack
和 debug_print_backtrace
方法去捕获到函数调用节点的栈帧跟踪信息。
function foo(string $testParameter) {
debug_print_backtrace();
}
foo('Hello');
#0 test.php: baz('Hello')
#1 test.php(4): bar('Hello')
#2 test.php(15): foo('Hello')
上述回溯信息显示了函数调用的行号,回溯到每个调用者的调用函数名几参数。
与 debug_print_backtrace
函数像上面这样打印栈帧信息相似,debug_backtrace
函数返回结构化数组,可用于进一步处理:
function foo(string $testParameter): void {
bar($testParameter);
}
function bar(string $testParameter): void {
baz($testParameter);
}
function baz(string $testParameter): void {
var_dump(debug_backtrace());
}
foo('Hello');
array(3) {
[0]=> array(4) {
["file"]=> string(38) "test.php"
["line"]=> int(8)
["function"]=> string(3) "baz"
["args"]=> array(1) {
[0]=> string(5) "Hello"
}
}
[1]=> array(4) {
["file"]=> string(38) "test.php"
["line"]=> int(4)
["function"]=> string(3) "bar"
["args"]=> array(1) {
[0]=> string(5) "Hello"
}
}
[2]=> array(4) {
["file"]=> string(38) "test.php"
["line"]=> int(15)
["function"]=> string(3) "foo"
["args"]=> array(1) {
[0]=> string(5) "Hello"
}
}
}
栈跟踪对调试和追溯检查很有用,同时也会有很大的安全隐患,因为包含了大量应用内的信息如文件结构(文件名及行号)以及每个栈桢中传入到函数中的参数等大量真实数据。
比如从 PHP 8.0 开始使用未知的 hash 算法调用 password_hash
函数会造成致命错误,而栈跟踪会暴露真实密码:
password_hash($password, 'unknown-algo');
Fatal error: Uncaught ValueError: password_hash(): Argument #2 ($algo) must be a valid password hashing algorithm in ...:...
Stack trace:
#0 ....php(4): password_hash('test', 'unknown-algo')
#1 {main}
thrown in ... on line ...
注意 栈桢 0 包含了 $password
变量的真实数据,可能会在错误信息、错误日志或应用日志中暴露,这样是不安全也不该发生的。
从 PHP 8.2 开始,可以使用 SensitiveParameter
注解对其脱敏。这样可以避免 PHP 栈跟踪泄露敏感信息。
function passwordHash(#[\SensitiveParameter] string $password) {
var_dump(debug_backtrace());
}
passwordHash('hunter2');
在支持敏感信息脱敏之前, PHP 会将未经修改的参数传入:
array(1) {
[0]=>
array(4) {
["file"]=> string(38) "..."
["line"]=> int(9)
["function"]=> string(3) "passwordHash"
["args"]=> array(1) {
[0]=> string(38) "hunter2"
}
}
}
使用参数值脱敏后,参数值会被 SensitiveParameterValue
对象替换,有效避免被标记为敏感参数(SensitiveParameter)的参数值泄露给错误日志、跟踪的栈等。
-function passwordHash(string $password): string {
+function passwordHash(#[\SensitiveParameter] string $password): string {
debug_print_backtrace();
}
passwordHash('hunter2');
array(1) {
[0]=>
array(4) {
["file"]=> string(38) "..."
["line"]=> int(9)
["function"]=> string(3) "foo"
["args"]=> array(1) {
- [0]=> string(38) "hunter2"
+ [0]=> object(SensitiveParameterValue)#1 (0) {
+ }
}
}
}
#[\SensitiveParameter]
注解
SensitiveParameter
是 PHP 8.2 中新加入内核的注解。在全局命名空间中进行了声明,能用于注解任何参数:#[\SensitiveParameter]
该注解只能用在参数中。
SensitiveParameter 概要:
#[Attribute(Attribute::TARGET_PARAMETER)]
final class SensitiveParameter {
public function __construct() {}
}
\SensitiveParameterValue
类
如果一个参数被注解,var_dump/logging
函数参数的实际值会被 PHP 8.2 中新增的 SensitiveParameterValue
对象所代替。
SensitiveParameterValue
是 PHP 8.2 中新增于全局命名空间的类,实例化 \SensitiveParameterValue
没有限制,不过不允许对其进行系列化。
\SensitiveParameterValue
对象压缩了参数的实际值,以防因正当理由需要获取实际值的情况。
SensitiveParameterValue 概要:
final class SensitiveParameterValue {
private readonly mixed $value;
public function __construct(mixed $value) {
$this->value = $value;
}
public function getValue(): mixed {
return $this->value;
}
public function __debugInfo(): array {
return [];
}
public function __serialize(): array {
throw new \Exception("Serialization of 'SensitiveParameterValue' is not allowed");
}
public function __unserialize(array $data): void {
throw new \Exception("Unserialization of 'SensitiveParameterValue' is not allowed");
}
}
- 只读属性是 PHP 8.1 添加的新特性。删除 readonly 标志实现向下兼容。
- mixed 是 PHP 8.0 添加的新类型。 删除 mixed 类型可以实现向下兼容。
- __serialize/__unserialize 魔术方法是 PHP 7.4 引入的。使用 __sleep and __wake 魔术方法可以向下兼容。
参数值脱敏后,实际值存在私有属性中。因为是私有的,无法在 \SensitiveParameterValue
类的外部访问它。__debugInfo
魔术方法返回空数组,使得值属性不包含在任何调试值中。
如果要访问实际值,调用 getValue()
方法:
$stackTrace = debug_backtrace();
var_dump($stackTrace[0]['args'][0]->getValue()); // "hunter2"
\SensitiveParameterValue
对象不允许系列化。
$stackTrace = debug_backtrace();
serialize($stackTrace);
Exception: Serialization of 'SensitiveParameterValue' is not allowed in ...:...
这一行为避免敏感信息被暴露在系列化字符串中,不过如果未被告知不能系列化,日志记录器可能会在尝试系列化栈跟踪信息时出现兼容问题。不过
SensitiveParameterValue
不是唯一不能被系列化的类,任何系列化栈跟踪信息的应用都必须避免系列化这样的类。
向后兼容性影响
SensitiveParameter
和 SensitiveParameterValue
是全局命名空间中新声明的类。用户空间应用不再允许用同一名字在全局命名空间中声明这些类。
可以在老版本中对 \SensitiveParameter
和 \SensitiveParameterValue
类进行 polyfill 兼容,不过 PHP 不会自动 使用 #[\SensitiveParameter]
注解进行脱敏。