diff --git a/src/DSN/DSN.php b/src/DSN/DSN.php index fd1111b..2661c63 100644 --- a/src/DSN/DSN.php +++ b/src/DSN/DSN.php @@ -51,12 +51,14 @@ class DSN * * @param string $dsn */ - public function __construct(string $dsn) + public function __construct(#[\SensitiveParameter] string $dsn) { $parts = \parse_url($dsn); + // PHP before 8.2 ignores SensitiveParameter, and stack traces print a parameter's current value + unset($dsn); if (!$parts) { - throw new \InvalidArgumentException("Unable to parse DSN: $dsn"); + throw new \InvalidArgumentException('Unable to parse DSN: malformed'); } if (empty($parts['scheme'])) { diff --git a/tests/DSN/DSNTest.php b/tests/DSN/DSNTest.php index 4521f37..9f92fc8 100644 --- a/tests/DSN/DSNTest.php +++ b/tests/DSN/DSNTest.php @@ -7,6 +7,10 @@ class DSNTest extends TestCase { + private const USER = 'AKIAKEY'; + + private const PASSWORD = 'SECRETKEY'; + public function testSuccess(): void { $dsn = new DSN('mariadb://user:password@localhost:3306/database?charset=utf8&timezone=UTC'); @@ -153,4 +157,54 @@ public function testFail(): void $this->expectException(\InvalidArgumentException::class); new DSN('mariadb://'); } + + /** + * @return array + */ + public static function provideRefusedDSNs(): array + { + $credentials = self::USER . ':' . self::PASSWORD; + + return [ + 'unparseable' => ["s3://{$credentials}@/backups?region=us-east-1"], + 'no scheme' => ["//{$credentials}@localhost/database"], + 'no host' => ["{$credentials}@localhost/database"], + ]; + } + + /** + * @dataProvider provideRefusedDSNs + */ + public function testRefusalMessageOmitsCredentials(string $dsn): void + { + try { + new DSN($dsn); + } catch (\InvalidArgumentException $exception) { + $this->assertStringNotContainsString(self::USER, $exception->getMessage()); + $this->assertStringNotContainsString(self::PASSWORD, $exception->getMessage()); + + return; + } + + $this->fail('The DSN was accepted'); + } + + public function testUncaughtRefusalPrintsNoCredentials(): void + { + $ignoreArgs = \ini_set('zend.exception_ignore_args', '0'); + $maxLength = \ini_set('zend.exception_string_param_max_len', '1000000'); + + try { + new DSN('s3://' . self::USER . ':' . self::PASSWORD . '@/backups?region=us-east-1'); + $this->fail('The DSN was accepted'); + } catch (\InvalidArgumentException $exception) { + $printed = (string) $exception; + } finally { + \ini_set('zend.exception_ignore_args', $ignoreArgs); + \ini_set('zend.exception_string_param_max_len', $maxLength); + } + + $this->assertStringNotContainsString(self::USER, $printed); + $this->assertStringNotContainsString(self::PASSWORD, $printed); + } }