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.
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.phptests/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 istestRedactsSecretNestedInAnArrayValueAlreadyInterpolatedIntoTheMessage).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, nohgbinary in this environment) and the same pre-existing single PHPUnit deprecation, confirmed identical viagit stashagainst a clean HEAD run.vendor/bin/phpstan analyse: no errors.- Counterfactual: reverted only the fix in
RedactingFormatter.php(kept the new test) viagit stashon 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.