From 4566f3ae586f4aed515500e90ecb7da77de9674f Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Tue, 15 Apr 2025 11:04:08 -0400 Subject: [PATCH] [HttpFoundation][FrameworkBundle] clock support for `UriSigner` --- .../Resources/config/services.php | 3 +++ .../Component/HttpFoundation/CHANGELOG.md | 1 + .../HttpFoundation/Tests/UriSignerTest.php | 24 +++++++++++++++++++ .../Component/HttpFoundation/UriSigner.php | 11 +++++++-- .../Component/HttpFoundation/composer.json | 1 + 5 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php index 558c2b6d52334..936867d542afb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php @@ -158,6 +158,9 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : [] ->set('uri_signer', UriSigner::class) ->args([ new Parameter('kernel.secret'), + '_hash', + '_expiration', + service('clock')->nullOnInvalid(), ]) ->lazy() ->alias(UriSigner::class, 'uri_signer') diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index 2d8065ba53e5a..5410cba632897 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Add `EventStreamResponse` and `ServerEvent` classes to streamline server event streaming * Add support for `valkey:` / `valkeys:` schemes for sessions * `Request::getPreferredLanguage()` now favors a more preferred language above exactly matching a locale + * Allow `UriSigner` to use a `ClockInterface` 7.2 --- diff --git a/src/Symfony/Component/HttpFoundation/Tests/UriSignerTest.php b/src/Symfony/Component/HttpFoundation/Tests/UriSignerTest.php index 927e2bda84db8..85a0b727ccda3 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/UriSignerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/UriSignerTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Clock\MockClock; use Symfony\Component\HttpFoundation\Exception\LogicException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\UriSigner; @@ -199,6 +200,29 @@ public function testCheckWithUriExpiration() $this->assertFalse($signer->check($relativeUriFromNow3)); } + public function testCheckWithUriExpirationWithClock() + { + $clock = new MockClock(); + $signer = new UriSigner('foobar', clock: $clock); + + $this->assertFalse($signer->check($signer->sign('http://example.com/foo', new \DateTimeImmutable('2000-01-01 00:00:00')))); + $this->assertFalse($signer->check($signer->sign('http://example.com/foo?foo=bar', new \DateTimeImmutable('2000-01-01 00:00:00')))); + $this->assertFalse($signer->check($signer->sign('http://example.com/foo?foo=bar&0=integer', new \DateTimeImmutable('2000-01-01 00:00:00')))); + + $this->assertFalse($signer->check($signer->sign('http://example.com/foo', 1577836800))); // 2000-01-01 + $this->assertFalse($signer->check($signer->sign('http://example.com/foo?foo=bar', 1577836800))); // 2000-01-01 + $this->assertFalse($signer->check($signer->sign('http://example.com/foo?foo=bar&0=integer', 1577836800))); // 2000-01-01 + + $relativeUriFromNow1 = $signer->sign('http://example.com/foo', new \DateInterval('PT3S')); + $relativeUriFromNow2 = $signer->sign('http://example.com/foo?foo=bar', new \DateInterval('PT3S')); + $relativeUriFromNow3 = $signer->sign('http://example.com/foo?foo=bar&0=integer', new \DateInterval('PT3S')); + $clock->sleep(10); + + $this->assertFalse($signer->check($relativeUriFromNow1)); + $this->assertFalse($signer->check($relativeUriFromNow2)); + $this->assertFalse($signer->check($relativeUriFromNow3)); + } + public function testNonUrlSafeBase64() { $signer = new UriSigner('foobar'); diff --git a/src/Symfony/Component/HttpFoundation/UriSigner.php b/src/Symfony/Component/HttpFoundation/UriSigner.php index 1c9e25a5c0151..b1109ae692326 100644 --- a/src/Symfony/Component/HttpFoundation/UriSigner.php +++ b/src/Symfony/Component/HttpFoundation/UriSigner.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpFoundation; +use Psr\Clock\ClockInterface; use Symfony\Component\HttpFoundation\Exception\LogicException; /** @@ -26,6 +27,7 @@ public function __construct( #[\SensitiveParameter] private string $secret, private string $hashParameter = '_hash', private string $expirationParameter = '_expiration', + private ?ClockInterface $clock = null, ) { if (!$secret) { throw new \InvalidArgumentException('A non-empty secret is required.'); @@ -109,7 +111,7 @@ public function check(string $uri): bool } if ($expiration = $params[$this->expirationParameter] ?? false) { - return time() < $expiration; + return $this->now()->getTimestamp() < $expiration; } return true; @@ -153,9 +155,14 @@ private function getExpirationTime(\DateTimeInterface|\DateInterval|int $expirat } if ($expiration instanceof \DateInterval) { - return \DateTimeImmutable::createFromFormat('U', time())->add($expiration)->format('U'); + return $this->now()->add($expiration)->format('U'); } return (string) $expiration; } + + private function now(): \DateTimeImmutable + { + return $this->clock?->now() ?? \DateTimeImmutable::createFromFormat('U', time()); + } } diff --git a/src/Symfony/Component/HttpFoundation/composer.json b/src/Symfony/Component/HttpFoundation/composer.json index cb2bbf8cbbeed..a86b21b7c728a 100644 --- a/src/Symfony/Component/HttpFoundation/composer.json +++ b/src/Symfony/Component/HttpFoundation/composer.json @@ -25,6 +25,7 @@ "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", "symfony/cache": "^6.4.12|^7.1.5", + "symfony/clock": "^6.4|^7.0", "symfony/dependency-injection": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/mime": "^6.4|^7.0",