Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit eb71515

Browse filesBrowse files
feature #60102 [HttpFoundation] Add UriSigner::verify() that throws named exceptions (kbond)
This PR was squashed before being merged into the 7.3 branch. Discussion ---------- [HttpFoundation] Add `UriSigner::verify()` that throws named exceptions | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | n/a | License | MIT This adds `UriSigner::verify()`: Similar to `check()` but throws `UnSignedUriException`, `UnverifiedSignedUriException` or `ExpiredSignedUriException` if invalid (no return if valid). Usage example: ```php // Verifying a Signed Uri try { $uriSigner->verify($uri); } catch (UnSignedUriException $e) { // the uri either not signed $e->uri; // the uri value } catch (UnverifiedSignedUriException $e) { // the the signature is invalid $e->uri; // the uri value } catch (ExpiredSignedUriException $e) { // the signature is valid but it's expired $e->uri; // the uri value $e->expiredAt; // \DateTimeImmutable } ``` **TODO** - [x] Changelog - [x] Tests Commits ------- ac4de2c [HttpFoundation] Add `UriSigner::verify()` that throws named exceptions
2 parents f30cc7b + ac4de2c commit eb71515
Copy full SHA for eb71515

File tree

7 files changed

+210
-24
lines changed
Filter options

7 files changed

+210
-24
lines changed

‎src/Symfony/Component/HttpFoundation/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpFoundation/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* Add support for `valkey:` / `valkeys:` schemes for sessions
1010
* `Request::getPreferredLanguage()` now favors a more preferred language above exactly matching a locale
1111
* Allow `UriSigner` to use a `ClockInterface`
12+
* Add `UriSigner::verify()`
1213

1314
7.2
1415
---
+26Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Exception;
13+
14+
/**
15+
* @author Kevin Bond <kevinbond@gmail.com>
16+
*/
17+
final class ExpiredSignedUriException extends SignedUriException
18+
{
19+
/**
20+
* @internal
21+
*/
22+
public function __construct()
23+
{
24+
parent::__construct('The URI has expired.');
25+
}
26+
}
+19Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Exception;
13+
14+
/**
15+
* @author Kevin Bond <kevinbond@gmail.com>
16+
*/
17+
abstract class SignedUriException extends \RuntimeException implements ExceptionInterface
18+
{
19+
}
+26Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Exception;
13+
14+
/**
15+
* @author Kevin Bond <kevinbond@gmail.com>
16+
*/
17+
final class UnsignedUriException extends SignedUriException
18+
{
19+
/**
20+
* @internal
21+
*/
22+
public function __construct()
23+
{
24+
parent::__construct('The URI is not signed.');
25+
}
26+
}
+26Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Exception;
13+
14+
/**
15+
* @author Kevin Bond <kevinbond@gmail.com>
16+
*/
17+
final class UnverifiedSignedUriException extends SignedUriException
18+
{
19+
/**
20+
* @internal
21+
*/
22+
public function __construct()
23+
{
24+
parent::__construct('The URI signature is invalid.');
25+
}
26+
}

‎src/Symfony/Component/HttpFoundation/Tests/UriSignerTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpFoundation/Tests/UriSignerTest.php
+33Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Clock\MockClock;
16+
use Symfony\Component\HttpFoundation\Exception\ExpiredSignedUriException;
1617
use Symfony\Component\HttpFoundation\Exception\LogicException;
18+
use Symfony\Component\HttpFoundation\Exception\UnsignedUriException;
19+
use Symfony\Component\HttpFoundation\Exception\UnverifiedSignedUriException;
1720
use Symfony\Component\HttpFoundation\Request;
1821
use Symfony\Component\HttpFoundation\UriSigner;
1922

@@ -228,4 +231,34 @@ public function testNonUrlSafeBase64()
228231
$signer = new UriSigner('foobar');
229232
$this->assertTrue($signer->check('http://example.com/foo?_hash=rIOcC%2FF3DoEGo%2FvnESjSp7uU9zA9S%2F%2BOLhxgMexoPUM%3D&baz=bay&foo=bar'));
230233
}
234+
235+
public function testVerifyUnSignedUri()
236+
{
237+
$signer = new UriSigner('foobar');
238+
$uri = 'http://example.com/foo';
239+
240+
$this->expectException(UnsignedUriException::class);
241+
242+
$signer->verify($uri);
243+
}
244+
245+
public function testVerifyUnverifiedUri()
246+
{
247+
$signer = new UriSigner('foobar');
248+
$uri = 'http://example.com/foo?_hash=invalid';
249+
250+
$this->expectException(UnverifiedSignedUriException::class);
251+
252+
$signer->verify($uri);
253+
}
254+
255+
public function testVerifyExpiredUri()
256+
{
257+
$signer = new UriSigner('foobar');
258+
$uri = $signer->sign('http://example.com/foo', 123456);
259+
260+
$this->expectException(ExpiredSignedUriException::class);
261+
262+
$signer->verify($uri);
263+
}
231264
}

‎src/Symfony/Component/HttpFoundation/UriSigner.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpFoundation/UriSigner.php
+79-24Lines changed: 79 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,22 @@
1212
namespace Symfony\Component\HttpFoundation;
1313

1414
use Psr\Clock\ClockInterface;
15+
use Symfony\Component\HttpFoundation\Exception\ExpiredSignedUriException;
1516
use Symfony\Component\HttpFoundation\Exception\LogicException;
17+
use Symfony\Component\HttpFoundation\Exception\SignedUriException;
18+
use Symfony\Component\HttpFoundation\Exception\UnsignedUriException;
19+
use Symfony\Component\HttpFoundation\Exception\UnverifiedSignedUriException;
1620

1721
/**
1822
* @author Fabien Potencier <fabien@symfony.com>
1923
*/
2024
class UriSigner
2125
{
26+
private const STATUS_VALID = 1;
27+
private const STATUS_INVALID = 2;
28+
private const STATUS_MISSING = 3;
29+
private const STATUS_EXPIRED = 4;
30+
2231
/**
2332
* @param string $hashParameter Query string parameter to use
2433
* @param string $expirationParameter Query string parameter to use for expiration
@@ -91,38 +100,40 @@ public function sign(string $uri/* , \DateTimeInterface|\DateInterval|int|null $
91100
*/
92101
public function check(string $uri): bool
93102
{
94-
$url = parse_url($uri);
95-
$params = [];
96-
97-
if (isset($url['query'])) {
98-
parse_str($url['query'], $params);
99-
}
103+
return self::STATUS_VALID === $this->doVerify($uri);
104+
}
100105

101-
if (empty($params[$this->hashParameter])) {
102-
return false;
103-
}
106+
public function checkRequest(Request $request): bool
107+
{
108+
return self::STATUS_VALID === $this->doVerify(self::normalize($request));
109+
}
104110

105-
$hash = $params[$this->hashParameter];
106-
unset($params[$this->hashParameter]);
111+
/**
112+
* Verify a Request or string URI.
113+
*
114+
* @throws UnsignedUriException If the URI is not signed
115+
* @throws UnverifiedSignedUriException If the signature is invalid
116+
* @throws ExpiredSignedUriException If the URI has expired
117+
* @throws SignedUriException
118+
*/
119+
public function verify(Request|string $uri): void
120+
{
121+
$uri = self::normalize($uri);
122+
$status = $this->doVerify($uri);
107123

108-
// In 8.0, remove support for non-url-safe tokens
109-
if (!hash_equals($this->computeHash($this->buildUrl($url, $params)), strtr(rtrim($hash, '='), ['/' => '_', '+' => '-']))) {
110-
return false;
124+
if (self::STATUS_VALID === $status) {
125+
return;
111126
}
112127

113-
if ($expiration = $params[$this->expirationParameter] ?? false) {
114-
return $this->now()->getTimestamp() < $expiration;
128+
if (self::STATUS_MISSING === $status) {
129+
throw new UnsignedUriException();
115130
}
116131

117-
return true;
118-
}
119-
120-
public function checkRequest(Request $request): bool
121-
{
122-
$qs = ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : '';
132+
if (self::STATUS_INVALID === $status) {
133+
throw new UnverifiedSignedUriException();
134+
}
123135

124-
// we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering)
125-
return $this->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().$qs);
136+
throw new ExpiredSignedUriException();
126137
}
127138

128139
private function computeHash(string $uri): string
@@ -165,4 +176,48 @@ private function now(): \DateTimeImmutable
165176
{
166177
return $this->clock?->now() ?? \DateTimeImmutable::createFromFormat('U', time());
167178
}
179+
180+
/**
181+
* @return self::STATUS_*
182+
*/
183+
private function doVerify(string $uri): int
184+
{
185+
$url = parse_url($uri);
186+
$params = [];
187+
188+
if (isset($url['query'])) {
189+
parse_str($url['query'], $params);
190+
}
191+
192+
if (empty($params[$this->hashParameter])) {
193+
return self::STATUS_MISSING;
194+
}
195+
196+
$hash = $params[$this->hashParameter];
197+
unset($params[$this->hashParameter]);
198+
199+
if (!hash_equals($this->computeHash($this->buildUrl($url, $params)), strtr(rtrim($hash, '='), ['/' => '_', '+' => '-']))) {
200+
return self::STATUS_INVALID;
201+
}
202+
203+
if (!$expiration = $params[$this->expirationParameter] ?? false) {
204+
return self::STATUS_VALID;
205+
}
206+
207+
if ($this->now()->getTimestamp() < $expiration) {
208+
return self::STATUS_VALID;
209+
}
210+
211+
return self::STATUS_EXPIRED;
212+
}
213+
214+
private static function normalize(Request|string $uri): string
215+
{
216+
if ($uri instanceof Request) {
217+
$qs = ($qs = $uri->server->get('QUERY_STRING')) ? '?'.$qs : '';
218+
$uri = $uri->getSchemeAndHttpHost().$uri->getBaseUrl().$uri->getPathInfo().$qs;
219+
}
220+
221+
return $uri;
222+
}
168223
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.