From 3686aef3feb4b9c49b4d9ad21e2172a8c9929f98 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 23 Jul 2026 18:09:04 +0200 Subject: [PATCH 1/2] feat(pii): add sensitive data scrubber --- src/DataCollection/SensitiveDataScrubber.php | 164 +++++++++++++++ .../SensitiveDataScrubberTest.php | 192 ++++++++++++++++++ 2 files changed, 356 insertions(+) create mode 100644 src/DataCollection/SensitiveDataScrubber.php create mode 100644 tests/DataCollection/SensitiveDataScrubberTest.php diff --git a/src/DataCollection/SensitiveDataScrubber.php b/src/DataCollection/SensitiveDataScrubber.php new file mode 100644 index 000000000..2c387e69e --- /dev/null +++ b/src/DataCollection/SensitiveDataScrubber.php @@ -0,0 +1,164 @@ + $headers + * + * @phpstan-param KeyValueCollectionBehavior $behavior + * + * @return array + */ + public static function scrubHeaders(array $headers, array $behavior): array + { + $scrubbed = []; + + foreach ($headers as $name => $values) { + $name = (string) $name; + + if (\in_array(strtolower($name), self::SENSITIVE_HEADERS, true) || self::shouldScrubValue($name, $behavior)) { + foreach ($values as $headerLine => $headerValue) { + $values[$headerLine] = '[Filtered]'; + } + } + + $scrubbed[$name] = $values; + } + + return $scrubbed; + } + + /** + * @param array $data + * + * @phpstan-param KeyValueCollectionBehavior $behavior + * + * @return array + */ + public static function scrubKeyValueData(array $data, array $behavior): array + { + $scrubbed = []; + + /** @mago-ignore analysis:mixed-assignment */ + foreach ($data as $key => $value) { + $key = (string) $key; + $scrubbed[$key] = self::shouldScrubValue($key, $behavior) ? '[Filtered]' : $value; + } + + return $scrubbed; + } + + /** + * @phpstan-param KeyValueCollectionBehavior $behavior + */ + public static function scrubQueryString(string $queryString, array $behavior): string + { + $parts = explode('&', $queryString); + + foreach ($parts as $index => $part) { + $separatorPosition = strpos($part, '='); + $encodedKey = $separatorPosition === false ? $part : substr($part, 0, $separatorPosition); + $key = urldecode($encodedKey); + + if (self::shouldScrubValue($key, $behavior)) { + $parts[$index] = $encodedKey . '=[Filtered]'; + } + } + + return implode('&', $parts); + } + + /** + * @phpstan-param KeyValueCollectionBehavior $behavior + */ + private static function shouldScrubValue(string $key, array $behavior): bool + { + if (self::matchesMandatoryDenyList($key)) { + return true; + } + + if ($behavior['mode'] === 'allowList') { + return !self::matchesAnyTerm($key, $behavior['terms'], false); + } + + return $behavior['terms'] !== [] && self::matchesAnyTerm($key, $behavior['terms'], true); + } + + private static function matchesMandatoryDenyList(string $key): bool + { + if (self::$sensitiveDataDenyListRegex === null) { + self::$sensitiveDataDenyListRegex = '/' . implode('|', array_map(static function (string $term): string { + return preg_quote($term, '/'); + }, self::SENSITIVE_DATA_DENYLIST)) . '/i'; + } + + return preg_match(self::$sensitiveDataDenyListRegex, $key) === 1; + } + + /** + * @param string[] $terms + */ + private static function matchesAnyTerm(string $key, array $terms, bool $partial): bool + { + $key = strtolower($key); + + foreach ($terms as $term) { + $term = strtolower($term); + + if (($partial && strpos($key, $term) !== false) || (!$partial && $key === $term)) { + return true; + } + } + + return false; + } +} diff --git a/tests/DataCollection/SensitiveDataScrubberTest.php b/tests/DataCollection/SensitiveDataScrubberTest.php new file mode 100644 index 000000000..78c6d94e5 --- /dev/null +++ b/tests/DataCollection/SensitiveDataScrubberTest.php @@ -0,0 +1,192 @@ + 'denyList', 'terms' => []]; + + $scrubbed = SensitiveDataScrubber::scrubKeyValueData([ + 'authorization' => 'secret', + 'public' => 'visible', + ], $behavior); + + $this->assertSame([ + 'authorization' => '[Filtered]', + 'public' => 'visible', + ], $scrubbed); + } + + public function testScrubCustomAndMandatory(): void + { + $behavior = ['mode' => 'denyList', 'terms' => ['custom']]; + + $scrubbed = SensitiveDataScrubber::scrubKeyValueData([ + 'authorization' => 'secret', + 'custom-field' => 'private', + 'public' => 'visible', + ], $behavior); + + $this->assertSame([ + 'authorization' => '[Filtered]', + 'custom-field' => '[Filtered]', + 'public' => 'visible', + ], $scrubbed); + } + + public function testScrubCaseInsensitiveKeys(): void + { + $behavior = ['mode' => 'denyList', 'terms' => []]; + + $scrubbed = SensitiveDataScrubber::scrubKeyValueData(['AUTHORIZATION' => 'secret'], $behavior); + + $this->assertSame(['AUTHORIZATION' => '[Filtered]'], $scrubbed); + } + + public function testAllowList(): void + { + $behavior = ['mode' => 'allowList', 'terms' => ['theme']]; + + $scrubbed = SensitiveDataScrubber::scrubKeyValueData([ + 'theme' => 'dark', + 'tracking_id' => '12345', + ], $behavior); + + $this->assertSame([ + 'theme' => 'dark', + 'tracking_id' => '[Filtered]', + ], $scrubbed); + } + + public function testScrubHeadersAppliesDenyList(): void + { + $behavior = ['mode' => 'denyList', 'terms' => []]; + + $scrubbed = SensitiveDataScrubber::scrubHeaders([ + 'Authorization' => ['secret'], + 'X-Request-Id' => ['request-id'], + ], $behavior); + + $this->assertSame([ + 'Authorization' => ['[Filtered]'], + 'X-Request-Id' => ['request-id'], + ], $scrubbed); + } + + public function testScrubHeadersScrubsEveryLineOfMatchingHeaders(): void + { + $behavior = ['mode' => 'denyList', 'terms' => []]; + + $scrubbed = SensitiveDataScrubber::scrubHeaders(['X-Api-Key' => ['first', 'second']], $behavior); + + $this->assertSame(['X-Api-Key' => ['[Filtered]', '[Filtered]']], $scrubbed); + } + + public function testScrubHeadersAlwaysScrubsCookieHeaders(): void + { + $behavior = ['mode' => 'denyList', 'terms' => []]; + + $scrubbed = SensitiveDataScrubber::scrubHeaders([ + 'Cookie' => ['session_id=secret; theme=dark'], + 'Set-Cookie' => ['session_id=secret'], + 'X-Request-Id' => ['request-id'], + ], $behavior); + + $this->assertSame([ + 'Cookie' => ['[Filtered]'], + 'Set-Cookie' => ['[Filtered]'], + 'X-Request-Id' => ['request-id'], + ], $scrubbed); + } + + public function testScrubHeadersAllowListCannotOverrideCookieHeaders(): void + { + $behavior = ['mode' => 'allowList', 'terms' => ['cookie', 'set-cookie']]; + + $scrubbed = SensitiveDataScrubber::scrubHeaders([ + 'Cookie' => ['session_id=secret'], + 'Set-Cookie' => ['session_id=secret'], + ], $behavior); + + $this->assertSame([ + 'Cookie' => ['[Filtered]'], + 'Set-Cookie' => ['[Filtered]'], + ], $scrubbed); + } + + public function testExtendedDenyTerms(): void + { + $defaultBehavior = ['mode' => 'denyList', 'terms' => []]; + $extendedBehavior = ['mode' => 'denyList', 'terms' => ['forwarded', '-ip', 'remote-', 'via', '-user']]; + $headers = [ + 'X-Forwarded-For' => ['203.0.113.7'], + 'X-Real-IP' => ['203.0.113.7'], + ]; + + $this->assertSame($headers, SensitiveDataScrubber::scrubHeaders($headers, $defaultBehavior)); + $this->assertSame([ + 'X-Forwarded-For' => ['[Filtered]'], + 'X-Real-IP' => ['[Filtered]'], + ], SensitiveDataScrubber::scrubHeaders($headers, $extendedBehavior)); + } + + public function testScrubHeadersAllowListCannotOverrideMandatoryDenyList(): void + { + $behavior = ['mode' => 'allowList', 'terms' => ['authorization', 'x-request-id']]; + + $scrubbed = SensitiveDataScrubber::scrubHeaders([ + 'Authorization' => ['secret'], + 'X-Request-Id' => ['request-id'], + 'Host' => ['example.com'], + ], $behavior); + + $this->assertSame([ + 'Authorization' => ['[Filtered]'], + 'X-Request-Id' => ['request-id'], + 'Host' => ['[Filtered]'], + ], $scrubbed); + } + + public function testScrubQueryStringAppliesMandatoryDenyList(): void + { + $behavior = ['mode' => 'denyList', 'terms' => []]; + + $scrubbed = SensitiveDataScrubber::scrubQueryString('token=secret&page=1', $behavior); + + $this->assertSame('token=[Filtered]&page=1', $scrubbed); + } + + public function testScrubQueryStringAppliesCustomDenyListTerms(): void + { + $behavior = ['mode' => 'denyList', 'terms' => ['page']]; + + $scrubbed = SensitiveDataScrubber::scrubQueryString('token=secret&page=1&flag', $behavior); + + $this->assertSame('token=[Filtered]&page=[Filtered]&flag', $scrubbed); + } + + public function testScrubQueryStringDecodesKeysBeforeMatching(): void + { + $behavior = ['mode' => 'denyList', 'terms' => []]; + + $scrubbed = SensitiveDataScrubber::scrubQueryString('api%5Ftoken=secret&page=1', $behavior); + + $this->assertSame('api%5Ftoken=[Filtered]&page=1', $scrubbed); + } + + public function testCookieNameIsAllowedInQueryParams(): void + { + $behaviour = ['mode' => 'denyList', 'terms' => []]; + + $scrubbed = SensitiveDataScrubber::scrubQueryString('cookie=foo&set-cookie=bar', $behaviour); + + $this->assertSame('cookie=foo&set-cookie=bar', $scrubbed); + } +} From 763ca041bbbde9a6d515f7c1f195ae6692b54a82 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 30 Jul 2026 15:54:52 -0400 Subject: [PATCH 2/2] handle off --- ...ataScrubber.php => KeyValueDataFilter.php} | 57 +++--- .../DataCollection/KeyValueDataFilterTest.php | 186 +++++++++++++++++ .../SensitiveDataScrubberTest.php | 192 ------------------ 3 files changed, 218 insertions(+), 217 deletions(-) rename src/DataCollection/{SensitiveDataScrubber.php => KeyValueDataFilter.php} (69%) create mode 100644 tests/DataCollection/KeyValueDataFilterTest.php delete mode 100644 tests/DataCollection/SensitiveDataScrubberTest.php diff --git a/src/DataCollection/SensitiveDataScrubber.php b/src/DataCollection/KeyValueDataFilter.php similarity index 69% rename from src/DataCollection/SensitiveDataScrubber.php rename to src/DataCollection/KeyValueDataFilter.php index 2c387e69e..c90c6d5ab 100644 --- a/src/DataCollection/SensitiveDataScrubber.php +++ b/src/DataCollection/KeyValueDataFilter.php @@ -9,7 +9,7 @@ * * @phpstan-type KeyValueCollectionBehavior array{mode: 'off'|'denyList'|'allowList', terms: string[]} */ -final class SensitiveDataScrubber +final class KeyValueDataFilter { private const SENSITIVE_DATA_DENYLIST = [ 'auth', @@ -32,7 +32,7 @@ final class SensitiveDataScrubber ]; /** - * cookie headers that we always want to redact. + * Cookie headers that must always be filtered when headers are collected. */ private const SENSITIVE_HEADERS = [ 'cookie', @@ -44,9 +44,6 @@ final class SensitiveDataScrubber */ private static $sensitiveDataDenyListRegex; - /** - * This class contains only static methods and should not be instantiated. - */ private function __construct() { } @@ -56,25 +53,29 @@ private function __construct() * * @phpstan-param KeyValueCollectionBehavior $behavior * - * @return array + * @return array|null Returns null when collection is off */ - public static function scrubHeaders(array $headers, array $behavior): array + public static function filterHeaders(array $headers, array $behavior): ?array { - $scrubbed = []; + if ($behavior['mode'] === 'off') { + return null; + } + + $filtered = []; foreach ($headers as $name => $values) { $name = (string) $name; - if (\in_array(strtolower($name), self::SENSITIVE_HEADERS, true) || self::shouldScrubValue($name, $behavior)) { + if (\in_array(strtolower($name), self::SENSITIVE_HEADERS, true) || self::shouldFilterValue($name, $behavior)) { foreach ($values as $headerLine => $headerValue) { $values[$headerLine] = '[Filtered]'; } } - $scrubbed[$name] = $values; + $filtered[$name] = $values; } - return $scrubbed; + return $filtered; } /** @@ -82,26 +83,34 @@ public static function scrubHeaders(array $headers, array $behavior): array * * @phpstan-param KeyValueCollectionBehavior $behavior * - * @return array + * @return array|null Returns null when collection is off */ - public static function scrubKeyValueData(array $data, array $behavior): array + public static function filterKeyValueData(array $data, array $behavior): ?array { - $scrubbed = []; + if ($behavior['mode'] === 'off') { + return null; + } + + $filtered = []; /** @mago-ignore analysis:mixed-assignment */ foreach ($data as $key => $value) { $key = (string) $key; - $scrubbed[$key] = self::shouldScrubValue($key, $behavior) ? '[Filtered]' : $value; + $filtered[$key] = self::shouldFilterValue($key, $behavior) ? '[Filtered]' : $value; } - return $scrubbed; + return $filtered; } /** * @phpstan-param KeyValueCollectionBehavior $behavior */ - public static function scrubQueryString(string $queryString, array $behavior): string + public static function filterQueryString(string $queryString, array $behavior): ?string { + if ($behavior['mode'] === 'off') { + return null; + } + $parts = explode('&', $queryString); foreach ($parts as $index => $part) { @@ -109,7 +118,7 @@ public static function scrubQueryString(string $queryString, array $behavior): s $encodedKey = $separatorPosition === false ? $part : substr($part, 0, $separatorPosition); $key = urldecode($encodedKey); - if (self::shouldScrubValue($key, $behavior)) { + if (self::shouldFilterValue($key, $behavior)) { $parts[$index] = $encodedKey . '=[Filtered]'; } } @@ -120,17 +129,17 @@ public static function scrubQueryString(string $queryString, array $behavior): s /** * @phpstan-param KeyValueCollectionBehavior $behavior */ - private static function shouldScrubValue(string $key, array $behavior): bool + private static function shouldFilterValue(string $key, array $behavior): bool { if (self::matchesMandatoryDenyList($key)) { return true; } if ($behavior['mode'] === 'allowList') { - return !self::matchesAnyTerm($key, $behavior['terms'], false); + return !self::matchesAnyTerm($key, $behavior['terms']); } - return $behavior['terms'] !== [] && self::matchesAnyTerm($key, $behavior['terms'], true); + return self::matchesAnyTerm($key, $behavior['terms']); } private static function matchesMandatoryDenyList(string $key): bool @@ -147,14 +156,12 @@ private static function matchesMandatoryDenyList(string $key): bool /** * @param string[] $terms */ - private static function matchesAnyTerm(string $key, array $terms, bool $partial): bool + private static function matchesAnyTerm(string $key, array $terms): bool { $key = strtolower($key); foreach ($terms as $term) { - $term = strtolower($term); - - if (($partial && strpos($key, $term) !== false) || (!$partial && $key === $term)) { + if (strpos($key, strtolower($term)) !== false) { return true; } } diff --git a/tests/DataCollection/KeyValueDataFilterTest.php b/tests/DataCollection/KeyValueDataFilterTest.php new file mode 100644 index 000000000..2662102cb --- /dev/null +++ b/tests/DataCollection/KeyValueDataFilterTest.php @@ -0,0 +1,186 @@ + 'off', 'terms' => ['public']]; + + $this->assertNull(KeyValueDataFilter::filterKeyValueData([ + 'authorization' => 'secret', + 'public' => 'visible', + ], $behavior)); + } + + public function testFilterKeyValueDataAppliesMandatoryDenyList(): void + { + $behavior = ['mode' => 'denyList', 'terms' => []]; + + $filtered = KeyValueDataFilter::filterKeyValueData([ + 'AUTHORIZATION' => 'secret', + 'public' => 'visible', + ], $behavior); + + $this->assertSame([ + 'AUTHORIZATION' => '[Filtered]', + 'public' => 'visible', + ], $filtered); + } + + public function testFilterKeyValueDataCombinesMandatoryAndCustomDenyListTerms(): void + { + $behavior = ['mode' => 'denyList', 'terms' => ['custom']]; + + $filtered = KeyValueDataFilter::filterKeyValueData([ + 'authorization' => 'secret', + 'custom-field' => 'private', + 'public' => 'visible', + ], $behavior); + + $this->assertSame([ + 'authorization' => '[Filtered]', + 'custom-field' => '[Filtered]', + 'public' => 'visible', + ], $filtered); + } + + public function testFilterKeyValueDataAppliesAllowList(): void + { + $behavior = ['mode' => 'allowList', 'terms' => ['theme']]; + + $filtered = KeyValueDataFilter::filterKeyValueData([ + 'preferred-theme' => 'dark', + 'tracking_id' => '12345', + ], $behavior); + + $this->assertSame([ + 'preferred-theme' => 'dark', + 'tracking_id' => '[Filtered]', + ], $filtered); + } + + public function testFilterKeyValueDataAllowListCannotOverrideMandatoryDenyList(): void + { + $behavior = ['mode' => 'allowList', 'terms' => ['authorization']]; + + $filtered = KeyValueDataFilter::filterKeyValueData([ + 'authorization' => 'secret', + ], $behavior); + + $this->assertSame(['authorization' => '[Filtered]'], $filtered); + } + + public function testFilterHeadersReturnsNullWhenCollectionIsOff(): void + { + $behavior = ['mode' => 'off', 'terms' => ['x-request-id']]; + + $this->assertNull(KeyValueDataFilter::filterHeaders([ + 'Authorization' => ['secret'], + 'X-Request-Id' => ['request-id'], + ], $behavior)); + } + + public function testFilterHeadersAppliesDenyListToEveryHeaderLine(): void + { + $behavior = ['mode' => 'denyList', 'terms' => []]; + + $filtered = KeyValueDataFilter::filterHeaders([ + 'X-Api-Key' => ['first', 'second'], + 'X-Request-Id' => ['request-id'], + ], $behavior); + + $this->assertSame([ + 'X-Api-Key' => ['[Filtered]', '[Filtered]'], + 'X-Request-Id' => ['request-id'], + ], $filtered); + } + + public function testFilterHeadersAlwaysFiltersCookieHeaders(): void + { + $behavior = ['mode' => 'allowList', 'terms' => ['cookie', 'set-cookie', 'x-request-id']]; + + $filtered = KeyValueDataFilter::filterHeaders([ + 'Cookie' => ['session_id=secret; theme=dark'], + 'Set-Cookie' => ['session_id=secret'], + 'X-Request-Id' => ['request-id'], + ], $behavior); + + $this->assertSame([ + 'Cookie' => ['[Filtered]'], + 'Set-Cookie' => ['[Filtered]'], + 'X-Request-Id' => ['request-id'], + ], $filtered); + } + + public function testFilterHeadersAppliesExtendedDenyTerms(): void + { + $defaultBehavior = ['mode' => 'denyList', 'terms' => []]; + $extendedBehavior = ['mode' => 'denyList', 'terms' => ['forwarded', '-ip', 'remote-', 'via', '-user']]; + $headers = [ + 'X-Forwarded-For' => ['203.0.113.7'], + 'X-Real-IP' => ['203.0.113.7'], + ]; + + $this->assertSame($headers, KeyValueDataFilter::filterHeaders($headers, $defaultBehavior)); + $this->assertSame([ + 'X-Forwarded-For' => ['[Filtered]'], + 'X-Real-IP' => ['[Filtered]'], + ], KeyValueDataFilter::filterHeaders($headers, $extendedBehavior)); + } + + public function testFilterHeadersAppliesAllowList(): void + { + $behavior = ['mode' => 'allowList', 'terms' => ['request-id']]; + + $filtered = KeyValueDataFilter::filterHeaders([ + 'X-Request-Id' => ['request-id'], + 'Host' => ['example.com'], + ], $behavior); + + $this->assertSame([ + 'X-Request-Id' => ['request-id'], + 'Host' => ['[Filtered]'], + ], $filtered); + } + + public function testFilterQueryStringReturnsNullWhenCollectionIsOff(): void + { + $behavior = ['mode' => 'off', 'terms' => ['page']]; + + $this->assertNull(KeyValueDataFilter::filterQueryString('token=secret&page=1', $behavior)); + } + + public function testFilterQueryStringAppliesMandatoryAndCustomDenyListTerms(): void + { + $behavior = ['mode' => 'denyList', 'terms' => ['page']]; + + $filtered = KeyValueDataFilter::filterQueryString('token=secret&page=1&flag', $behavior); + + $this->assertSame('token=[Filtered]&page=[Filtered]&flag', $filtered); + } + + public function testFilterQueryStringDecodesKeysBeforeMatching(): void + { + $behavior = ['mode' => 'denyList', 'terms' => []]; + + $filtered = KeyValueDataFilter::filterQueryString('api%5Ftoken=secret&page=1', $behavior); + + $this->assertSame('api%5Ftoken=[Filtered]&page=1', $filtered); + } + + public function testFilterQueryStringDoesNotTreatCookieNamesAsCookieHeaders(): void + { + $behavior = ['mode' => 'denyList', 'terms' => []]; + + $filtered = KeyValueDataFilter::filterQueryString('cookie=foo&set-cookie=bar', $behavior); + + $this->assertSame('cookie=foo&set-cookie=bar', $filtered); + } +} diff --git a/tests/DataCollection/SensitiveDataScrubberTest.php b/tests/DataCollection/SensitiveDataScrubberTest.php deleted file mode 100644 index 78c6d94e5..000000000 --- a/tests/DataCollection/SensitiveDataScrubberTest.php +++ /dev/null @@ -1,192 +0,0 @@ - 'denyList', 'terms' => []]; - - $scrubbed = SensitiveDataScrubber::scrubKeyValueData([ - 'authorization' => 'secret', - 'public' => 'visible', - ], $behavior); - - $this->assertSame([ - 'authorization' => '[Filtered]', - 'public' => 'visible', - ], $scrubbed); - } - - public function testScrubCustomAndMandatory(): void - { - $behavior = ['mode' => 'denyList', 'terms' => ['custom']]; - - $scrubbed = SensitiveDataScrubber::scrubKeyValueData([ - 'authorization' => 'secret', - 'custom-field' => 'private', - 'public' => 'visible', - ], $behavior); - - $this->assertSame([ - 'authorization' => '[Filtered]', - 'custom-field' => '[Filtered]', - 'public' => 'visible', - ], $scrubbed); - } - - public function testScrubCaseInsensitiveKeys(): void - { - $behavior = ['mode' => 'denyList', 'terms' => []]; - - $scrubbed = SensitiveDataScrubber::scrubKeyValueData(['AUTHORIZATION' => 'secret'], $behavior); - - $this->assertSame(['AUTHORIZATION' => '[Filtered]'], $scrubbed); - } - - public function testAllowList(): void - { - $behavior = ['mode' => 'allowList', 'terms' => ['theme']]; - - $scrubbed = SensitiveDataScrubber::scrubKeyValueData([ - 'theme' => 'dark', - 'tracking_id' => '12345', - ], $behavior); - - $this->assertSame([ - 'theme' => 'dark', - 'tracking_id' => '[Filtered]', - ], $scrubbed); - } - - public function testScrubHeadersAppliesDenyList(): void - { - $behavior = ['mode' => 'denyList', 'terms' => []]; - - $scrubbed = SensitiveDataScrubber::scrubHeaders([ - 'Authorization' => ['secret'], - 'X-Request-Id' => ['request-id'], - ], $behavior); - - $this->assertSame([ - 'Authorization' => ['[Filtered]'], - 'X-Request-Id' => ['request-id'], - ], $scrubbed); - } - - public function testScrubHeadersScrubsEveryLineOfMatchingHeaders(): void - { - $behavior = ['mode' => 'denyList', 'terms' => []]; - - $scrubbed = SensitiveDataScrubber::scrubHeaders(['X-Api-Key' => ['first', 'second']], $behavior); - - $this->assertSame(['X-Api-Key' => ['[Filtered]', '[Filtered]']], $scrubbed); - } - - public function testScrubHeadersAlwaysScrubsCookieHeaders(): void - { - $behavior = ['mode' => 'denyList', 'terms' => []]; - - $scrubbed = SensitiveDataScrubber::scrubHeaders([ - 'Cookie' => ['session_id=secret; theme=dark'], - 'Set-Cookie' => ['session_id=secret'], - 'X-Request-Id' => ['request-id'], - ], $behavior); - - $this->assertSame([ - 'Cookie' => ['[Filtered]'], - 'Set-Cookie' => ['[Filtered]'], - 'X-Request-Id' => ['request-id'], - ], $scrubbed); - } - - public function testScrubHeadersAllowListCannotOverrideCookieHeaders(): void - { - $behavior = ['mode' => 'allowList', 'terms' => ['cookie', 'set-cookie']]; - - $scrubbed = SensitiveDataScrubber::scrubHeaders([ - 'Cookie' => ['session_id=secret'], - 'Set-Cookie' => ['session_id=secret'], - ], $behavior); - - $this->assertSame([ - 'Cookie' => ['[Filtered]'], - 'Set-Cookie' => ['[Filtered]'], - ], $scrubbed); - } - - public function testExtendedDenyTerms(): void - { - $defaultBehavior = ['mode' => 'denyList', 'terms' => []]; - $extendedBehavior = ['mode' => 'denyList', 'terms' => ['forwarded', '-ip', 'remote-', 'via', '-user']]; - $headers = [ - 'X-Forwarded-For' => ['203.0.113.7'], - 'X-Real-IP' => ['203.0.113.7'], - ]; - - $this->assertSame($headers, SensitiveDataScrubber::scrubHeaders($headers, $defaultBehavior)); - $this->assertSame([ - 'X-Forwarded-For' => ['[Filtered]'], - 'X-Real-IP' => ['[Filtered]'], - ], SensitiveDataScrubber::scrubHeaders($headers, $extendedBehavior)); - } - - public function testScrubHeadersAllowListCannotOverrideMandatoryDenyList(): void - { - $behavior = ['mode' => 'allowList', 'terms' => ['authorization', 'x-request-id']]; - - $scrubbed = SensitiveDataScrubber::scrubHeaders([ - 'Authorization' => ['secret'], - 'X-Request-Id' => ['request-id'], - 'Host' => ['example.com'], - ], $behavior); - - $this->assertSame([ - 'Authorization' => ['[Filtered]'], - 'X-Request-Id' => ['request-id'], - 'Host' => ['[Filtered]'], - ], $scrubbed); - } - - public function testScrubQueryStringAppliesMandatoryDenyList(): void - { - $behavior = ['mode' => 'denyList', 'terms' => []]; - - $scrubbed = SensitiveDataScrubber::scrubQueryString('token=secret&page=1', $behavior); - - $this->assertSame('token=[Filtered]&page=1', $scrubbed); - } - - public function testScrubQueryStringAppliesCustomDenyListTerms(): void - { - $behavior = ['mode' => 'denyList', 'terms' => ['page']]; - - $scrubbed = SensitiveDataScrubber::scrubQueryString('token=secret&page=1&flag', $behavior); - - $this->assertSame('token=[Filtered]&page=[Filtered]&flag', $scrubbed); - } - - public function testScrubQueryStringDecodesKeysBeforeMatching(): void - { - $behavior = ['mode' => 'denyList', 'terms' => []]; - - $scrubbed = SensitiveDataScrubber::scrubQueryString('api%5Ftoken=secret&page=1', $behavior); - - $this->assertSame('api%5Ftoken=[Filtered]&page=1', $scrubbed); - } - - public function testCookieNameIsAllowedInQueryParams(): void - { - $behaviour = ['mode' => 'denyList', 'terms' => []]; - - $scrubbed = SensitiveDataScrubber::scrubQueryString('cookie=foo&set-cookie=bar', $behaviour); - - $this->assertSame('cookie=foo&set-cookie=bar', $scrubbed); - } -}