From adf45f1d53ea76ac29313de64581ece398c4b87d Mon Sep 17 00:00:00 2001 From: Yuri Gaidoba Date: Wed, 25 Mar 2026 12:51:38 +0100 Subject: [PATCH] PLT-1764: Sanitize sensitive fields in FormatterTrait object normalization Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 4 + src/Service/Formatter/FormatterTrait.php | 16 +++ tests/Unit/FormatterTraitTest.php | 157 +++++++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 tests/Unit/FormatterTraitTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f88386..5c72d6a 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.3.0 +### Added +- Sanitization of sensitive fields (password, secret, apiKey, etc.) in `FormatterTrait` object normalization + ## 3.2.1 ### Fixed - Changed versions for graylog2/gelf-php diff --git a/src/Service/Formatter/FormatterTrait.php b/src/Service/Formatter/FormatterTrait.php index 4ea7f7f..bb57f2d 100644 --- a/src/Service/Formatter/FormatterTrait.php +++ b/src/Service/Formatter/FormatterTrait.php @@ -13,6 +13,16 @@ trait FormatterTrait { + /** @var string[] */ + private static $sensitiveKeys = [ + 'password', + 'secret', + 'apikey', + 'apisecret', + 'apisecretkey', + 'secretkey', + 'credentials', + ]; /** * Normalizes given data with pre-processing for Doctrine entities and collections. * @@ -96,6 +106,12 @@ private function normalizeObject($data) continue; } + $normalizedKey = preg_replace('/[^a-z]/', '', strtolower($fixedKey)); + if (in_array($normalizedKey, self::$sensitiveKeys, true)) { + $result[$fixedKey] = '***'; + continue; + } + $result[$fixedKey] = $value; } diff --git a/tests/Unit/FormatterTraitTest.php b/tests/Unit/FormatterTraitTest.php new file mode 100644 index 0000000..489f9a6 --- /dev/null +++ b/tests/Unit/FormatterTraitTest.php @@ -0,0 +1,157 @@ +$propertyName = 'sensitive_value'; + $object->name = 'visible'; + + $result = $this->normalizeObject($object); + + $this->assertSame('***', $result[$propertyName]); + $this->assertSame('visible', $result['name']); + } + + public function sensitiveKeysProvider(): array + { + return [ + 'password' => ['password'], + 'secret' => ['secret'], + 'apiKey' => ['apiKey'], + 'apiSecret' => ['apiSecret'], + 'secretKey' => ['secretKey'], + 'credentials' => ['credentials'], + ]; + } + + /** + * @dataProvider caseInsensitiveProvider + */ + public function testSensitiveKeysAreCaseInsensitive(string $propertyName) + { + $object = new \stdClass(); + $object->$propertyName = 'sensitive_value'; + + $result = $this->normalizeObject($object); + + $this->assertSame('***', $result[$propertyName]); + } + + public function caseInsensitiveProvider(): array + { + return [ + 'Password' => ['Password'], + 'PASSWORD' => ['PASSWORD'], + 'pAsSwOrD' => ['pAsSwOrD'], + 'SECRET' => ['SECRET'], + 'ApiKey' => ['ApiKey'], + 'APIKEY' => ['APIKEY'], + 'Credentials' => ['Credentials'], + ]; + } + + /** + * @dataProvider separatorVariantsProvider + */ + public function testSensitiveKeysWithSeparators(string $propertyName) + { + $object = new \stdClass(); + $object->$propertyName = 'sensitive_value'; + + $result = $this->normalizeObject($object); + + $this->assertSame('***', $result[$propertyName]); + } + + public function separatorVariantsProvider(): array + { + return [ + 'api_key' => ['api_key'], + 'api-key' => ['api-key'], + 'api.key' => ['api.key'], + 'api_secret' => ['api_secret'], + 'api-secret' => ['api-secret'], + 'secret_key' => ['secret_key'], + 'secret-key' => ['secret-key'], + 'api_secret_key' => ['api_secret_key'], + 'api-secret-key' => ['api-secret-key'], + 'API_KEY' => ['API_KEY'], + 'API_SECRET' => ['API_SECRET'], + 'SECRET_KEY' => ['SECRET_KEY'], + ]; + } + + public function testNonSensitivePropertiesPassThrough() + { + $object = new \stdClass(); + $object->name = 'John'; + $object->email = 'john@example.com'; + $object->age = 30; + $object->active = true; + + $result = $this->normalizeObject($object); + + $this->assertSame('John', $result['name']); + $this->assertSame('john@example.com', $result['email']); + $this->assertSame(30, $result['age']); + $this->assertTrue($result['active']); + } + + public function testDoubleUnderscorePrefixedPropertiesAreExcluded() + { + $object = new \stdClass(); + $object->__internal = 'hidden'; + $object->name = 'visible'; + + $result = $this->normalizeObject($object); + + $this->assertArrayNotHasKey('__internal', $result); + $this->assertSame('visible', $result['name']); + } + + public function testMixedSensitiveAndNonSensitiveProperties() + { + $object = new \stdClass(); + $object->username = 'admin'; + $object->password = 'super_secret'; + $object->email = 'admin@example.com'; + $object->apiKey = 'key-123'; + $object->role = 'admin'; + $object->credentials = ['token' => 'abc']; + + $result = $this->normalizeObject($object); + + $this->assertSame('admin', $result['username']); + $this->assertSame('***', $result['password']); + $this->assertSame('admin@example.com', $result['email']); + $this->assertSame('***', $result['apiKey']); + $this->assertSame('admin', $result['role']); + $this->assertSame('***', $result['credentials']); + } + + private function normalizeObject(object $data): array + { + $formatter = new class { + use FormatterTrait; + + public function callNormalizeObject(object $data): array + { + return $this->normalizeObject($data); + } + }; + + return $formatter->callNormalizeObject($data); + } +}