PHP 8.2: Sensitive Parameter value redaction support

Version8.2
TypeNew Feature

PHP, just like other programming languages, supports tracing the call stack at any point of the program. Stack tracing is commonly used when debugging an application, because it allows tracing back the functions/methods that lead to the point the stack trace is performed.

Each frame of a stack trace contains the function name and parameters. Obtaining a stack trace does not halt the program, and there are many use cases where stack traces are captured and logged silently for inspection without halting the application.

When an exception is thrown, the exception object also contains the stack trace up to the point the exception is thrown. Additionally, PHP provides built-in debug_backtrace and debug_print_backtrace functions to capture the stack trace up until the point those functions are called.



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')

The backtrace above shows the line number where the function call is made, leading to each caller, along with the called function name and parameters.

Similar to the debug_print_backtrace function printing the stack trace as shown above, debug_backtrace function returns a structured array that can be further processed:

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"
    }
  }
}

Just as the stack traces are useful in debugging and retrospective inspections, they can be a huge security concern because they contain a lot of information about the application's inner workings such the file structure (with file names and line numbers) as well as the actual data as each stack frame contains the parameters passed to a function call.

For instance, calling password_hash function with an unrecognized hashing algorithm name causes a fatal error since PHP 8.0, and that stack trace reveals the actual password in the stack trace:

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 ...

Notice how the stack frame zero contains the actual value of the $password variable, which can end up on error messages, error logs, application logs, etc., which is insecure and highly undesired.


In PHP 8.2 and later, it is possible to mark such sensitive parameters with a PHP attribute named SensitiveParameter, which makes PHP redact the sensitive information from the stack trace.

function passwordHash(#[\SensitiveParameter] string $password) {
    var_dump(debug_backtrace());
}

passwordHash('hunter2');

Prior to the support for redacting sensitive parameters, PHP simply passes the value of the parameters with no modifications:

array(1) {
  [0]=>
  array(4) {
    ["file"]=> string(38) "..."
    ["line"]=> int(9)
    ["function"]=> string(3) "passwordHash"
    ["args"]=> array(1) {
      [0]=> string(38) "hunter2"
    }
  }
}

With parameter value redacting, the value of the parameter is replaced with a SensitiveParameterValue object that effectively prevents the value of parameters marked SensitiveParameter from leaking to error logs, stack traces, etc.

-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] Attribute

SensitiveParameter is a new PHP attribute added to PHP core in PHP 8.2. It is declared in the global namespace, and can be used as an attribute for any parameter: #[\SensitiveParameter]

This attribute can only be used on parameters.

SensitiveParameter synopsis:

#[Attribute(Attribute::TARGET_PARAMETER)]
final class SensitiveParameter {
    public function __construct() {}
}

\SensitiveParameterValue Class

When a parameter is attributed as sensitive (with SensitiveParameter attribute), the actual values of the parameters available in var_dump/logging functions are replaced with an object of the new SensitiveParameterValue added in PHP 8.2.

SensitiveParameterValue is a new class added in PHP 8.2 in the global namespace. There are no restrictions on instantiating \SensitiveParameterValue instances, however, serializing them is not allowed.

\SensitiveParameterValue objects encapsulate the actual value of the parameter, in case the actual value needs to be deliberately obtained for legitimate use cases.

SensitiveParameterValue synopsis:

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");
  }
}

When a parameter value is redacted, the actual value is stored in the private property value. Because the value property is marked private, it cannot be accessed from outside the \SensitiveParameterValue class. The __debugInfo magic method returns an empty array, which makes sure the value property is not included in any debug values.

To access the actual value, call getValue() method:

$stackTrace = debug_backtrace();
var_dump($stackTrace[0]['args'][0]->getValue()); // "hunter2"

Serialization of \SensitiveParameterValue objects is not allowed

It is not allowed to serialize SensitiveParameterValue objects:

$stackTrace = debug_backtrace();
serialize($stackTrace);
Exception: Serialization of 'SensitiveParameterValue' is not allowed in ...:...

This behavior prevents the sensitive value from being exposed in the serialized string, but may introduce compatibility issues on loggers that attempt to serialize the stack trace without being aware of potential values that must not be serialized. However, SensitiveParameterValue is not the only class that cannot be serialized, and any application that serializes stack traces must make sure to avoid serializing such class objects.

Backwards Compatibility Impact

SensitiveParameter and SensitiveParameterValue are newly declared classes in the global namespace. User-land applications are no longer allowed to declare classes in the global namespace with the same names.

It is possible to polyfill \SensitiveParameter and \SensitiveParameterValue classes themselves in older PHP versions, but PHP will not automatically redact parameters attributed with #[\SensitiveParameter].


RFC Discussion Implementation