编程

PHP 8.2 新特性 — 敏感参数值脱敏支持

1251 2022-11-14 10:39:38

像其他编程语言一样,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] 注解进行脱敏。