| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Unit\Logging;
- use App\Infrastructure\Logging\SafeTrace;
- use PHPUnit\Framework\TestCase;
- use RuntimeException;
- use Throwable;
- /**
- * Regression test for SEC_REVIEW F21: PHP's getTraceAsString() inlines
- * scalar args into each frame, which leaks plaintext secrets when an
- * exception is thrown from inside `password_verify`, an OIDC token
- * validation call, etc. SafeTrace must drop args entirely.
- */
- final class SafeTraceTest extends TestCase
- {
- public function testFormattedTraceDoesNotContainArgs(): void
- {
- $secret = 'super-plaintext-password-please-do-not-leak';
- $caught = $this->capture(fn () => $this->throwWithSecret($secret));
- $trace = SafeTrace::format($caught);
- self::assertStringNotContainsString($secret, $trace);
- self::assertStringContainsString('throwWithSecret', $trace);
- }
- public function testFormattedTraceWalksPreviousChain(): void
- {
- $innerSecret = 'inner-secret-123';
- $outerSecret = 'outer-secret-456';
- $caught = $this->capture(function () use ($innerSecret, $outerSecret): void {
- try {
- $this->throwWithSecret($innerSecret);
- } catch (Throwable $inner) {
- $this->wrap($outerSecret, $inner);
- }
- });
- $trace = SafeTrace::format($caught);
- self::assertStringNotContainsString($innerSecret, $trace);
- self::assertStringNotContainsString($outerSecret, $trace);
- self::assertStringContainsString('Caused by', $trace);
- }
- public function testFrameLayoutResemblesPhpNativeFormat(): void
- {
- $caught = $this->capture(fn () => $this->throwWithSecret('x'));
- $trace = SafeTrace::format($caught);
- // Numbered frames, file(line) suffix, class::method() call, then {main}.
- self::assertMatchesRegularExpression('/^#0 .+\(\d+\): .+\(\)$/m', $trace);
- self::assertStringEndsWith('{main}', $trace);
- }
- private function capture(callable $fn): Throwable
- {
- try {
- $fn();
- } catch (Throwable $e) {
- return $e;
- }
- self::fail('expected callable to throw');
- }
- private function throwWithSecret(string $secret): never
- {
- throw new RuntimeException('boom');
- }
- private function wrap(string $secret, Throwable $inner): never
- {
- throw new RuntimeException('outer', 0, $inner);
- }
- }
|