diff --git a/src/Monolog/Formatter/RedactingFormatter.php b/src/Monolog/Formatter/RedactingFormatter.php index ed766d2..d97a66a 100644 --- a/src/Monolog/Formatter/RedactingFormatter.php +++ b/src/Monolog/Formatter/RedactingFormatter.php @@ -203,8 +203,9 @@ final class RedactingFormatter implements WrappingFormatterInterface if (\is_array($data)) { foreach ($data as $key => $value) { 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); + // the value is masked in the record, but a nested array's leaves can still + // leak elsewhere (e.g. json-encoded by PsrLogMessageProcessor) + $this->collectSensitiveValue($value, $secrets, $depth + 1); continue; } @@ -239,6 +240,28 @@ final class RedactingFormatter implements WrappingFormatterInterface } } + /** + * Collects every string leaf nested inside a value under a sensitive key. + * + * @param list $secrets + */ + private function collectSensitiveValue(mixed $value, array &$secrets, int $depth): void + { + if ($depth > self::MAX_DEPTH) { + return; + } + + if (\is_array($value)) { + foreach ($value as $item) { + $this->collectSensitiveValue($item, $secrets, $depth + 1); + } + + return; + } + + $this->addSecret($value, $secrets); + } + /** * Reflects the properties of an object matching a #[SensitiveParameter] constructor parameter * diff --git a/tests/Monolog/Formatter/RedactingFormatterTest.php b/tests/Monolog/Formatter/RedactingFormatterTest.php index ef4349c..f8c365a 100644 --- a/tests/Monolog/Formatter/RedactingFormatterTest.php +++ b/tests/Monolog/Formatter/RedactingFormatterTest.php @@ -243,6 +243,22 @@ class RedactingFormatterTest extends MonologTestCase $this->assertStringContainsString('calling api with [REDACTED]', $output); } + public function testRedactsSecretNestedInAnArrayValueAlreadyInterpolatedIntoTheMessage() + { + // PsrLogMessageProcessor json-encodes array context into the message, so a + // secret nested under a sensitive key leaks before this formatter runs. + $formatter = new RedactingFormatter(new LineFormatter('%message% %context%', 'Y-m-d')); + $processor = new PsrLogMessageProcessor(); + + $output = $formatter->format($processor($this->getRecord( + message: 'password reset payload: {password}', + context: ['password' => ['new' => 'tok-supersecret-value']], + ))); + + $this->assertStringNotContainsString('tok-supersecret-value', $output); + $this->assertStringContainsString('[REDACTED]', $output); + } + public function testRedactsSecretEmbeddedInAnExceptionMessage() { $formatter = new RedactingFormatter(new JsonFormatter());