Skip to content

SensitiveParameter 属性

概述

#[\SensitiveParameter] 是 PHP 8.2 引入的内置属性(Attribute),用于标记函数参数中的敏感信息。被标记的参数在异常堆栈跟踪(Stack Trace)中会被自动替换为 [sensitive] 占位符,防止密码、密钥、Token 等敏感信息泄露到日志中。

PHP 版本说明

  • #[\SensitiveParameter]:PHP 8.2+ 引入
  • 归属于 SensitiveParameter 命名空间,完整写法:#[\SensitiveParameter]
  • 仅影响堆栈跟踪输出,不影响函数内部逻辑和正常使用

基础概念

问题背景

在 PHP 8.2 之前,当函数抛出异常时,堆栈跟踪中会完整记录所有函数参数的值:

php
function connect(string $host, string $password) {
    throw new Exception('连接失败');
}

connect('db.example.com', 'my_secret_password');

堆栈跟踪输出:

Exception: 连接失败 in /app/src/Database.php:10
Stack trace:
#0 /app/src/Database.php(10): connect('db.example.com', 'my_secret_password')
                                                    ^^^^^^^^^^^^^^^^^^^^
                                                    密码泄露!

如果堆栈跟踪被记录到日志文件或展示给用户,敏感信息就会泄露。#[\SensitiveParameter] 解决了这个问题。

SensitiveParameter 的原理

#[\SensitiveParameter] 属性仅作用于堆栈跟踪的生成过程中——PHP 在构建堆栈帧(Stack Frame)时,检查参数是否有此属性,有则将值替换为 [sensitive]。它不影响:

  • 函数内部对参数的访问
  • 参数的正常传递和使用
  • 函数的返回值

语法与代码

基本用法

php
<?php

declare(strict_types=1);

use SensitiveParameter;

function connectToDatabase(
    string $host,
    #[SensitiveParameter]
    string $password,
    int $port = 3306
): void
{
    // 函数内部可以正常使用 $password
    echo "连接到 {$host}:{$port}" . PHP_EOL;
    echo "密码长度: " . strlen($password) . PHP_EOL;

    // 模拟连接失败
    throw new \RuntimeException('数据库连接超时');
}

try {
    connectToDatabase('db.example.com', 'super_secret_pass_123');
} catch (\RuntimeException $e) {
    // 堆栈跟踪中密码被隐藏
    echo $e->getTraceAsString();
    // #0 /app/example.php(18): connectToDatabase('db.example.com', '[sensitive]', 3306)
    //                                                         ^^^^^^^^^^^^^^^^
    //                                                         已被隐藏
}

在类方法中使用

php
<?php

declare(strict_types=1);

use SensitiveParameter;

class PaymentService
{
    public function processPayment(
        string $orderId,
        #[SensitiveParameter]
        string $creditCardNumber,
        #[SensitiveParameter]
        string $cvv,
        #[SensitiveParameter]
        string $expiryDate,
        float $amount
    ): array {
        // 内部正常使用敏感参数
        $lastFour = substr($creditCardNumber, -4);
        echo "处理订单 {$orderId},卡尾号 {$lastFour},金额 {$amount}" . PHP_EOL;

        throw new \RuntimeException('支付网关超时');
    }
}

try {
    $service = new PaymentService();
    $service->processPayment('ORD-001', '4111111111111111', '123', '12/26', 99.99);
} catch (\RuntimeException $e) {
    echo $e->getTraceAsString();
    // #0 ... PaymentService->processPayment(
    //     'ORD-001', '[sensitive]', '[sensitive]', '[sensitive]', 99.99
    // )
}

在构造函数中使用

php
<?php

declare(strict_types=1);

use SensitiveParameter;

final class DatabaseConfig
{
    public function __construct(
        public readonly string $host,
        public readonly int $port,
        #[SensitiveParameter]
        public readonly string $password,
        public readonly string $database,
    ) {}

    public function getDsn(): string
    {
        // password 仍然可以在内部正常使用
        return "mysql:host={$this->host};port={$this->port};dbname={$this->database}";
    }

    public function connect(): void
    {
        throw new \RuntimeException('连接失败');
    }
}

try {
    $config = new DatabaseConfig('localhost', 3306, 'db_password_123', 'myapp');
    $config->connect();
} catch (\RuntimeException $e) {
    // getTrace 中 password 参数显示为 [sensitive]
    echo $e->getTraceAsString();
}

在闭包/箭头函数中使用

php
<?php

declare(strict_types=1);

use SensitiveParameter;

// 闭包中使用
$authenticate = function (
    string $username,
    #[SensitiveParameter]
    string $password
): bool {
    return $username === 'admin' && $password === 'secret';
};

try {
    throw new \RuntimeException('认证服务不可用');
} catch (\RuntimeException $e) {
    // 闭包的堆栈跟踪也会隐藏敏感参数
    echo $e->getTraceAsString();
}

// 箭头函数中使用
$hashPassword = fn (
    #[SensitiveParameter]
    string $plainPassword
): string => password_hash($plainPassword, PASSWORD_BCRYPT);

$hash = $hashPassword('my_password'); // 正常使用
echo '哈希值: ' . $hash;

敏感参数与 set_exception_handler

php
<?php

declare(strict_types=1);

use SensitiveParameter;

// 全局异常处理器中,堆栈跟踪也会隐藏敏感参数
set_exception_handler(function (\Throwable $e): void {
    $trace = $e->getTraceAsString();
    error_log($trace); // 写入日志时敏感参数已被替换

    // 确保用户看不到堆栈跟踪
    echo json_encode(['error' => '服务器内部错误']);
});

function login(
    string $username,
    #[SensitiveParameter]
    string $password
): void {
    throw new \RuntimeException('认证失败');
}

login('admin', 'super_secret');
// 日志中记录的堆栈跟踪:
// login('admin', '[sensitive]')

详细说明

SensitiveParameter 属性定义

SensitiveParameter 的属性类定义非常简单:

php
// PHP 内部定义(简化版)
#[Attribute(Attribute::TARGET_PARAMETER)]
final class SensitiveParameter
{
    // 无需任何参数
}

关键信息:

  • 作用目标:仅限于参数(Attribute::TARGET_PARAMETER
  • 可重复:否(每个参数只需标记一次)
  • 继承:不能被继承或重写

getTrace() 与 getTraceAsString() 的区别

#[\SensitiveParameter] 对两种方法都有效:

php
<?php

declare(strict_types=1);

use SensitiveParameter;

function test(
    #[SensitiveParameter]
    string $secret
): void {
    throw new \Exception('test');
}

try {
    test('password123');
} catch (\Exception $e) {
    // getTraceAsString() — 文本格式
    echo $e->getTraceAsString();
    // #0 /app/test.php(11): test('[sensitive]')

    echo PHP_EOL;

    // getTrace() — 结构化数组
    $trace = $e->getTrace();
    print_r($trace[0]['args'][0]);
    // [sensitive]
}

受影响的输出位置

场景是否隐藏
$e->getTraceAsString()
$e->getTrace()是(args 中的值被替换)
set_exception_handler 日志
debug_backtrace()(不受影响)
var_dump($secret)(不影响正常变量输出)
函数内部使用 $secret(不影响正常逻辑)

debug_backtrace 不受影响

#[\SensitiveParameter] 仅影响异常堆栈跟踪。debug_backtrace() 函数不受此属性影响,仍然会显示所有参数值。

PHP 8.2 前的替代方案

在 PHP 8.2 之前,需要手动处理敏感参数的日志脱敏:

php
<?php

declare(strict_types=1);

class SensitiveTraceFilter
{
    /** @var array<int> 需要隐藏的参数位置索引 */
    private array $sensitivePositions;

    public function __construct(int ...$positions)
    {
        $this->sensitivePositions = $positions;
    }

    public function filterTrace(string $traceString): string
    {
        return preg_replace_callback(
            "/'([^']*)'/",
            function (array $matches): string {
                static $index = 0;
                $index++;
                if (in_array($index, $this->sensitivePositions, true)) {
                    return "'[FILTERED]'";
                }
                return $matches[0];
            },
            $traceString
        );
    }
}

PHP 8.2+ 后,不再需要这种手动过滤方案。

实战示例

安全的 API 客户端

php
<?php

declare(strict_types=1);

use SensitiveParameter;

final class ApiClient
{
    private string $baseUrl;
    private string $apiKey;

    public function __construct(
        string $baseUrl,
        #[SensitiveParameter]
        string $apiKey,
    ) {
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->apiKey = $apiKey;
    }

    public function request(
        string $method,
        string $endpoint,
        array $data = [],
        #[SensitiveParameter]
        string $accessToken = ''
    ): array {
        $url = $this->baseUrl . $endpoint;

        $headers = ['Content-Type: application/json'];
        if ($accessToken !== '') {
            $headers[] = "Authorization: Bearer {$accessToken}";
        }

        throw new \RuntimeException("API 请求失败: {$method} {$url}");
    }
}

try {
    $client = new ApiClient('https://api.example.com', 'sk_live_abc123');
    $client->request('POST', '/users', ['name' => 'Alice'], 'tok_xyz789');
} catch (\RuntimeException $e) {
    // apiKey 和 accessToken 都被隐藏
    error_log($e->getTraceAsString());
    // ApiClient->request('POST', '/users', [...], '[sensitive]')
    echo json_encode(['error' => '请求失败']);
}

注意事项

  1. 仅影响异常堆栈#[\SensitiveParameter] 不会影响 var_dumpprint_rerror_log 等其他输出方式。

  2. debug_backtrace 不受影响:在堆栈调试时,debug_backtrace() 仍然会显示完整参数值。不要依赖 #[\SensitiveParameter] 来保护 debug_backtrace 的输出。

  3. 敏感数据存储#[\SensitiveParameter] 只是防止日志泄露,不替代加密存储。密码等敏感数据仍需哈希后存储。

  4. PHP 8.2 最低要求:如果你的项目需要兼容 PHP 8.1 及以下版本,不能使用此属性。可通过条件判断或 polyfill 处理。

  5. toString() 方法$e->__toString() 输出的堆栈跟踪也会隐藏敏感参数,与 getTraceAsString() 行为一致。

最佳实践

推荐做法

  1. 所有密码/密钥参数都标记$password$apiKey$token$secret 等参数一律使用 #[\SensitiveParameter]
  2. 在公开 API 的构造函数中使用:第三方代码抛出异常时,确保你的密钥不会泄露
  3. 配合安全编码规范#[\SensitiveParameter] 是纵深防御的一层,不是唯一措施
  4. 审计日志内容:定期检查日志输出,确认敏感参数确实被隐藏
  5. 自定义异常格式化器中也要处理:如果你自定义了异常格式化输出,需要配合敏感参数过滤
php
<?php

declare(strict_types=1);

use SensitiveParameter;

// 最佳实践:为敏感参数命名约定添加文档注释
class AuthService
{
    /**
     * 用户认证
     *
     * @param string $username 用户名
     * @param string $password 用户密码(标记为敏感参数)
     */
    public function authenticate(
        string $username,
        #[SensitiveParameter]
        string $password,
    ): bool {
        // 验证逻辑...
        return true;
    }

    /**
     * 生成 API Token
     *
     * @param string $userId 用户 ID
     * @param string $secret 签名密钥(标记为敏感参数)
     */
    public function generateToken(
        string $userId,
        #[SensitiveParameter]
        string $secret,
    ): string {
        return hash_hmac('sha256', $userId, $secret);
    }
}

参考链接