Files
Seldaek-monolog/PR.md
T
dualfroz 7f7dc9b07c Fix RedactingFormatter missing secrets nested in array-valued sensitive keys
RedactingFormatter::collect() only added a sensitive key's value to the
secret list when it was a string or Stringable. When the value was an
array, addSecret() silently dropped it, so none of its leaf strings were
collected. PsrLogMessageProcessor json-encodes array context values when
interpolating them into the message, so a secret nested that way could
already be sitting in the message in plain text by the time this formatter
ran, and the output sweep had nothing to remove it with.

Add collectSensitiveValue() to walk a sensitive key's value and collect
every string leaf, however deeply nested, and use it instead of calling
addSecret() directly.
2026-09-04 08:38:50 +02:00

3.9 KiB

Fix RedactingFormatter missing secrets nested inside array-valued sensitive keys

Problem

RedactingFormatter masks a sensitive context/extra key by replacing its whole value with the mask in the record, and separately sweeps the wrapped formatter's rendered output for the original secret values, specifically to catch copies that leak through other channels such as PsrLogMessageProcessor interpolating a value into the message before the formatter ever runs (see the class docblock and the existing testRedactsSecretAlreadyInterpolatedIntoTheMessage test).

That sweep only works if the secret value was collected first. RedactingFormatter::collect() only calls addSecret() on the value of a sensitive key directly:

if (\is_string($key) && \in_array(strtolower($key), $this->sensitiveKeys, true)) {
    // no need to recurse, the whole value is masked in the record anyway
    $this->addSecret($value, $secrets);
    continue;
}

addSecret() only accepts strings and Stringable objects. When the value of a sensitive key is an array, addSecret() silently does nothing with it, so none of its leaf strings are ever added to the $secrets list used later during the sweep.

PsrLogMessageProcessor::__invoke() JSON-encodes array context values when interpolating them into {placeholder} tokens in the message:

} elseif (\is_array($val)) {
    $replacements[$placeholder] = 'array'.Utils::jsonEncode($val, null, true);
}

So a call such as:

$log->info('password reset payload: {password}', ['password' => ['new' => 'CorrectHorseBatteryStaple42']]);

ends up with the message password reset payload: array{"new":"CorrectHorseBatteryStaple42"} before RedactingFormatter ever sees the record. The context key gets masked correctly ("password":"[REDACTED]"), but the secret embedded in the message is never swept, because it was never collected: the final formatted output still contains CorrectHorseBatteryStaple42 in plain text.

Root cause: src/Monolog/Formatter/RedactingFormatter.php, collect() at line 205-210 (pre-fix), which calls addSecret() directly on a sensitive key's value instead of recursing into it when it is an array.

Fix

Added a small helper, collectSensitiveValue(), that walks a value already known to be sensitive (because it sits under a matched key) and collects every string leaf it contains, however deeply nested, respecting the same MAX_DEPTH guard used elsewhere in the class for self-referencing structures. collect() now calls this helper instead of addSecret() directly for a sensitive key's value.

Files changed:

  • src/Monolog/Formatter/RedactingFormatter.php
  • tests/Monolog/Formatter/RedactingFormatterTest.php (new regression test)

Verification

  • vendor/bin/phpunit tests/Monolog/Formatter/RedactingFormatterTest.php: 38/38 tests, 74 assertions, exit 0 (was 37/37 before this change; the new test is testRedactsSecretNestedInAnArrayValueAlreadyInterpolatedIntoTheMessage).
  • vendor/bin/phpunit (full suite): 1277 tests, 2368 assertions, exit 0. Same 37 skipped tests as an unmodified checkout (missing ext-mongodb/ext-redis, no local Elasticsearch, no ZendServer, no hg binary in this environment) and the same pre-existing single PHPUnit deprecation, confirmed identical via git stash against a clean HEAD run.
  • vendor/bin/phpstan analyse: no errors.
  • Counterfactual: reverted only the fix in RedactingFormatter.php (kept the new test) via git stash on that one file. The new test then fails with: Failed asserting that 'password reset payload: array{"new":"tok-supersecret-value"} {"password":"[REDACTED]"}' does not contain "tok-supersecret-value". Restoring the fix turns the suite green again (38/38).

Flags for account owner

None. No CONTRIBUTING.md, AGENTS.md/CLAUDE.md, or AI-authorship policy was found in this repository, and no CLA/DCO check runs in its CI workflows.