skip to content

In PHP 8.2 and later, what does the #[\SensitiveParameter] attribute redact, and what does it leave exposed?

level: middleimportance: should knowfreq 22%

answer

  1. backtrace arguments only
  2. replaced by a SensitiveParameterValue object
  3. debug_backtrace() and Exception::getTrace()
  4. already on password_hash() and PDO::__construct()
  5. a callee's own frame is not covered

basics

~20 s

It replaces the marked argument with a SensitiveParameterValue object in every backtrace PHP builds, so exception traces and debug_backtrace() no longer print it. The function body, var_dump(), logging and callees without the attribute still see the real value.

solid answer

~40 s

Added in PHP 8.2, `#[\SensitiveParameter]` goes on a **parameter** (it allows only `TARGET_PARAMETER`). Whenever PHP collects a backtrace — an exception's trace, `debug_backtrace()`, `debug_print_backtrace()` — that argument is swapped for an object of `SensitiveParameterValue`, which prints as `Object(SensitiveParameterValue)`, dumps empty, cannot be serialized or cast to string, and hands back the original only through `getValue()`. Built-in functions already use it: `password_hash()`, `password_verify()`, `crypt()` and the password parameter of `PDO::__construct()`. It is **not** encryption or access control: the function can still log or `var_dump()` the value, and if it passes the secret to another function whose parameter lacks the attribute, that callee's frame shows it in plain text.

code

php · 20 lines
php
<?php
declare(strict_types=1);

function openSocket(string $host, string $secret): void
{
    throw new RuntimeException('connect failed');
}

function connect(string $host, #[\SensitiveParameter] string $password): void
{
    openSocket($host, $password);   // callee does not mark $secret
}

try {
    connect('db', 's3cret');
} catch (RuntimeException $e) {
    echo $e->getTraceAsString(), PHP_EOL;
}
// #0 ...: openSocket('db', 's3cret')                         <- leaked
// #1 ...: connect('db', Object(SensitiveParameterValue))

go deeper

for a junior

Know that marking a password parameter with #[\SensitiveParameter] keeps it out of stack traces printed by exceptions.

for a middle

Explain the SensitiveParameterValue replacement, which trace sources it covers, and that it is parameter-only and was added in PHP 8.2.

for a senior

Audit the whole call chain a secret travels through, keep secrets out of exception messages, and combine the attribute with production trace settings.

for a principal

Set a codebase policy for secrets in diagnostics: which parameters must be marked, how tests enforce it, and what error trackers are allowed to receive.

## The problem it solves A PHP backtrace records, for each stack frame, the function and its **arguments**. That is useful for debugging and dangerous in production: a database connection that fails inside `connect(string $dsn, string $user, string $password)` produces an exception whose trace, rendered by `Exception::__toString()` or shipped to an error tracker, contains the password. Error pages, log files and monitoring tools then store the secret. PHP 8.2 added the built-in **`#[\SensitiveParameter]`** attribute so a function author can mark such parameters. ## What it does - It is declared with `Attribute::TARGET_PARAMETER` only; placing it on a method or property is a compile-time error. - When PHP builds a backtrace, the marked argument is replaced by an instance of **`SensitiveParameterValue`**. - This covers every backtrace the engine collects: `Exception::getTrace()` and `getTraceAsString()`, the string form of an uncaught exception, `debug_backtrace()` and `debug_print_backtrace()`. - In printed traces the argument shows as `Object(SensitiveParameterValue)`. `SensitiveParameterValue` is built to be hard to leak: | Operation | Result | |---|---| | `var_dump()` / `print_r()` | an empty object, via `__debugInfo()` | | `(string)` cast | `Error`: could not be converted to string | | `serialize()` | `Exception`: serialization is not allowed | | `getValue()` | the original argument, for code that truly needs it | ## Where PHP already uses it The engine's own stubs mark the obvious secrets: the password of `password_hash()`, `password_verify()` and `crypt()`, the `$password` of `PDO::__construct()` and of the `mysqli` connection functions. So a failed `new PDO($dsn, $user, $pass)` does not put `$pass` in the trace. ## What it does not protect The attribute changes backtraces and nothing else. It does not stop: 1. **the function's own code** — `var_dump($password)` or `$logger->info($password)` inside the function prints the real value; 2. **callees without the attribute** — if `connect()` passes `$password` to `openSocket(string $secret)` and `openSocket` does not mark `$secret`, the `openSocket` frame in the trace shows the plain string; 3. **other channels** — request dumps, `$_POST` logging, or an exception *message* you built with the secret in it; 4. **errors raised after the value was copied elsewhere**, such as into a property that a debugger or dump prints. So the rule is to mark the parameter at **every** level the secret passes through, and to keep secrets out of exception messages. ## How it relates to trace-argument settings PHP also has the ini setting `zend.exception_ignore_args`, built-in default `Off`, set to `On` in `php.ini-production`. When on, exception traces carry no arguments at all. That is a blunt, environment-wide switch; `#[\SensitiveParameter]` is precise and travels with the code, so it still protects secrets in development and in environments that leave arguments on for debugging. The two are complementary. ## Error trackers and logs Most production setups send uncaught exceptions to a log file or an error-tracking service, often together with the stack trace and its arguments. `#[\SensitiveParameter]` protects that path only if the reporting code builds its payload from PHP's trace (`getTrace()`), because that is where the substitution happens. Two things still leak: - a reporter that captures **local variables** or the request body by other means; - an exception **message** that interpolates the secret, such as `"login failed for $user with $password"` — messages are plain strings and never redacted. A useful habit is to treat the attribute as defence in depth: mark the parameter, keep secrets out of messages, and configure the tracker to scrub known field names as well. ## A quick review checklist - Passwords, API keys, tokens, private keys and card numbers in parameters get the attribute. - Wrappers and helpers that forward a secret get it too. - Constructor-promoted parameters holding secrets get it on the parameter. - Tests can assert redaction by throwing inside the function and checking the trace contains `SensitiveParameterValue`.

  • How does code that genuinely needs the original argument get it from a PHP backtrace?
    The trace's args entry holds a `SensitiveParameterValue`; calling `getValue()` on it returns the original argument. That is deliberate friction: dumping, string-casting and serializing the wrapper all refuse to reveal the value.
  • If php.ini-production already sets zend.exception_ignore_args to On, why mark parameters at all?
    That setting removes arguments from exception traces only where it is on; development, staging or a misconfigured host may leave it off, and `debug_backtrace()` output is separate. `#[\SensitiveParameter]` travels with the code and redacts that one argument everywhere.

saying these in an interview costs you the question

  • #[\SensitiveParameter] encrypts the argument while the function runs.
  • var_dump() of the parameter inside the function prints it redacted.
  • Marking the outer function protects the value in every callee's frame too.
  • The attribute can be placed on a method to redact all its arguments.
  • PHP's own password_hash() needs a wrapper to keep passwords out of traces.