8000 [HttpFoundation] Add temporary URI signed · symfony/symfony@76e4625 · GitHub
[go: up one dir, main page]

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 76e4625

Browse files
[HttpFoundation] Add temporary URI signed
1 parent 89fdb22 commit 76e4625

File tree

3 files changed

+140
-4
lines changed

3 files changed

+140
-4
lines changed

src/Symfony/Component/HttpFoundation/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ CHANGELOG
99
* Add `UriSigner` from the HttpKernel component
1010
* Add `partitioned` flag to `Cookie` (CHIPS Cookie)
1111
* Add argument `bool $flush = true` to `Response::send()`
12+
* Add optional `$expirationParameter` argument to `UriSigner::__construct()`
13+
* Add optional `$expiration` argument to `UriSigner::sign()`
1214

1315
6.3
1416
---

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

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,38 @@ public function testSign()
2424
$this->assertStringContainsString('?_hash=', $signer->sign('http://example.com/foo'));
2525
$this->assertStringContainsString('?_hash=', $signer->sign('http://example.com/foo?foo=bar'));
2626
$this->assertStringContainsString('&foo=', $signer->sign('http://example.com/foo?foo=bar'));
27+
28+
$this->assertStringContainsString('?_expiration=', $signer->sign('http://example.com/foo', 1));
29+
$this->assertStringContainsString('&_hash=', $signer->sign('http://example.com/foo', 1));
30+
$this->assertStringContainsString('?_expiration=', $signer->sign('http://example.com/foo?foo=bar', 1));
31+
$this->assertStringContainsString('&_hash=', $signer->sign('http://example.com/foo?foo=bar', 1));
32+
$this->assertStringContainsString('&foo=', $signer->sign('http://example.com/foo?foo=bar', 1));
2733
}
2834

2935
public function testCheck()
3036
{
3137
$signer = new UriSigner('foobar');
3238

39+
$this->assertFalse($signer->check('http://example.com/foo'));
3340
$this->assertFalse($signer->check('http://example.com/foo?_hash=foo'));
3441
$this->assertFalse($signer->check('http://example.com/foo?foo=bar&_hash=foo'));
3542
$this->assertFalse($signer->check('http://example.com/foo?foo=bar&_hash=foo&bar=foo'));
3643

44+
$this->assertFalse($signer->check('http://example.com/foo?_expiration=4070908800'));
45+
$this->assertFalse($signer->check('http://example.com/foo?_expiration=4070908800?_hash=foo'));
46+
$this->assertFalse($signer->check('http://example.com/foo?_expiration=4070908800&foo=bar&_hash=foo'));
47+
$this->assertFalse($signer->check('http://example.com/foo?_expiration=4070908800&foo=bar&_hash=foo&bar=foo'));
48+
3749
$this->assertTrue($signer->check($signer->sign('http://example.com/foo')));
3850
$this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar')));
3951
$this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&0=integer')));
4052

53+
$this->assertTrue($signer->check($signer->sign('http://example.com/foo', new \DateTimeImmutable('2099-01-01 00:00:00'))));
54+
$this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar', new \DateTimeImmutable('2099-01-01 00:00:00'))));
55+
$this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&0=integer', new \DateTimeImmutable('2099-01-01 00:00:00'))));
56+
4157
$this->assertSame($signer->sign('http://example.com/foo?foo=bar&bar=foo'), $signer->sign('http://example.com/foo?bar=foo&foo=bar'));
58+
$this->assertSame($signer->sign('http://example.com/foo?foo=bar&bar=foo', 1), $signer->sign('http://example.com/foo?bar=foo&foo=bar', 1) 57A7 );
4259
}
4360

4461
public function testCheckWithDifferentArgSeparator()
@@ -51,6 +68,12 @@ public function testCheckWithDifferentArgSeparator()
5168
$signer->sign('http://example.com/foo?foo=bar&baz=bay')
5269
);
5370
$this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&baz=bay')));
71+
72+
$this->assertSame(
73+
'http://example.com/foo?_expiration=4070908800&_hash=xfui5FoP0vbD9Cp7pI0tHnqR1Fmj2UARqkIUw7SZVfQ%3D&baz=bay&foo=bar',
74+
$signer->sign('http://example.com/foo?foo=bar&baz=bay', new \DateTimeImmutable('2099-01-01 00:00:00'))
75+
);
76+
$this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&baz=bay', new \DateTimeImmutable('2099-01-01 00:00:00'))));
5477
}
5578

5679
public function testCheckWithRequest()
@@ -60,17 +83,27 @@ public function testCheckWithRequest()
6083
$this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo'))));
6184
$this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo?foo=bar'))));
6285
$this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo?foo=bar&0=integer'))));
86+
87+
$this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo', new \DateTimeImmutable('2099-01-01 00:00:00')))));
88+
$this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo?foo=bar', new \DateTimeImmutable('2099-01-01 00:00:00')))));
89+
$this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo?foo=bar&0=integer', new \DateTimeImmutable('2099-01-01 00:00:00')))));
6390
}
6491

6592
public function testCheckWithDifferentParameter()
6693
{
67-
$signer = new UriSigner('foobar', 'qux');
94+
$signer = new UriSigner('foobar', 'qux', 'abc');
6895

6996
$this->assertSame(
7097
'http://example.com/foo?baz=bay&foo=bar&qux=rIOcC%2FF3DoEGo%2FvnESjSp7uU9zA9S%2F%2BOLhxgMexoPUM%3D',
7198
$signer->sign('http://example.com/foo?foo=bar&baz=bay')
7299
);
73100
$this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&baz=bay')));
101+
102+
$this->assertSame(
103+
'http://example.com/foo?abc=4070908800&baz=bay&foo=bar&qux=hdhUhBVPpzKJdz5ZjC%2FkLvtOYdGKOvKVOczmmMIZK0A%3D',
104+
$signer->sign('http://example.com/foo?foo=bar&baz=bay', new \DateTimeImmutable('2099-01-01 00:00:00'))
105+
);
106+
$this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&baz=bay', new \DateTimeImmutable('2099-01-01 00:00:00'))));
74107
}
75108

76109
public function testSignerWorksWithFragments()
@@ -81,6 +114,61 @@ public function testSignerWorksWithFragments()
81114
'http://example.com/foo?_hash=EhpAUyEobiM3QTrKxoLOtQq5IsWyWedoXDPqIjzNj5o%3D&bar=foo&foo=bar#foobar',
82115
$signer->sign('http://example.com/foo?bar=foo&foo=bar#foobar')
83116
);
117+
84118
$this->assertTrue($signer->check($signer->sign('http://example.com/foo?bar=foo&foo=bar#foobar')));
119+
120+
$this->assertSame(
121+
'http://example.com/foo?_expiration=4070908800&_hash=qHl626U5d7LMsVtBxPt9GNzysdSxyOQ1fHA59Y1ib0Y%3D&bar=foo&foo=bar#foobar',
122+
$signer->sign('http://example.com/foo?bar=foo&foo=bar#foobar', new \DateTimeImmutable('2099-01-01 00:00:00'))
123+
);
124+
125+
$this->assertTrue($signer->check($signer->sign('http://example.com/foo?bar=foo&foo=bar#foobar', new \DateTimeImmutable('2099-01-01 00:00:00'))));
126+
}
127+
128+
public function testSignWithUriExpiration()
129+
{
130+
$signer = new UriSigner('foobar');
131+
132+
$this->assertSame($signer->sign('http://example.com/foo?foo=bar&bar=foo', new \DateTimeImmutable('2099-01-01 00:00:00')), $signer->sign('http://example.com/foo?bar=foo&foo=bar', 4070908800));
133+
}
134+
135+
public function testSignWithoutExpirationAndWithReservedParameter()
136+
{
137+
$signer = new UriSigner('foobar');
138+
139+
$this->expectException(\LogicException::class);
140+
141+
$signer->sign('http://example.com/foo?_expiration=4070908800');
142+
}
143+
144+
public function testSignWithExpirationAndWithReservedParameter()
145+
{
146+
$signer = new UriSigner('foobar');
147+
148+
$this->expectException(\LogicException::class);
149+
150+
$signer->sign('http://example.com/foo?_expiration=4070908800', new \DateTimeImmutable('2099-01-01 00:00:00'));
151+
}
152+
153+
public function testCheckWithUriExpiration()
154+
{
155+
$signer = new UriSigner('foobar');
156+
157+
$this->assertFalse($signer->check($signer->sign('http://example.com/foo', new \DateTimeImmutable('2000-01-01 00:00:00'))));
158+
$this->assertFalse($signer->check($signer->sign('http://example.com/foo?foo=bar', new \DateTimeImmutable('2000-01-01 00:00:00'))));
159+
$this->assertFalse($signer->check($signer->sign('http://example.com/foo?foo=bar&0=integer', new \DateTimeImmutable('2000-01-01 00:00:00'))));
160+
161+
$this->assertFalse($signer->check($signer->sign('http://example.com/foo', 1577836800))); // 2000-01-01
162+
$this->assertFalse($signer->check($signer->sign('http://example.com/foo?foo=bar', 1577836800))); // 2000-01-01
163+
$this->assertFalse($signer->check($signer->sign('http://example.com/foo?foo=bar&0=integer', 1577836800))); // 2000-01-01
164+
165+
$relativeUriFromNow1 = $signer->sign('http://example.com/foo', new \DateInterval('PT3S'));
166+
$relativeUriFromNow2 = $signer->sign('http://example.com/foo?foo=bar', new \DateInterval('PT3S'));
167+
$relativeUriFromNow3 = $signer->sign('http://example.com/foo?foo=bar&0=integer', new \DateInterval('PT3S'));
168+
sleep(10);
169+
170+
$this->assertFalse($signer->check($relativeUriFromNow1));
171+
$this->assertFalse($signer->check($relativeUriFromNow2));
172+
$this->assertFalse($signer->check($relativeUriFromNow3));
85173
}
86174
}

src/Symfony/Component/HttpFoundation/UriSigner.php

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,35 @@ class UriSigner
2020
{
2121
private string $secret;
2222
private string $parameter;
23+
private string $expirationParameter;
2324

2425
/**
2526
* @param string $secret A secret
2627
* @param string $parameter Query string parameter to use
28+
* @param string $expirationParameter Query string parameter to use for expiration
2729
*/
28-
public function __construct(#[\SensitiveParameter] string $secret, string $parameter = '_hash')
30+
public function __construct(#[\SensitiveParameter] string $secret, string $parameter = '_hash', string $expirationParameter = '_expiration')
2931
{
3032
$this->secret = $secret;
3133
$this->parameter = $parameter;
34+
$this->expirationParameter = $expirationParameter;
3235
}
3336

3437
/**
3538
* Signs a URI.
3639
*
3740
* The given URI is signed by adding the query string parameter
3841
* which value depends on the URI and the secret.
42+
*
43+
* @param \DateTimeInterface|\DateInterval|int|null $expiration The expiration for the given URI.
44+
* If $expiration is a \DateTimeInterface, it's expected to be the exact date + time.
45+
* If $expiration is a \DateInterval, the interval is added to "now" to get the date + time.
46+
* If $expiration is an int, it's expected to be a timestamp in seconds of the exact date + time.
47+
* If $expiration is null, no expiration.
48+
*
49+
* The expiration is added as a query string parameter.
3950
*/
40-
public function sign(string $uri): string
51+
public function sign(string $uri, \DateTimeInterface|\DateInterval|int $expiration = null): string
4152
{
4253
$url = parse_url($uri);
4354
$params = [];
@@ -46,6 +57,14 @@ public function sign(string $uri): string
4657
parse_str($url['query'], $params);
4758
}
4859

60+
if (isset($params[$this->expirationParameter])) {
61+
throw new \LogicException(sprintf('URI query parameter conflict. Parameter name "%s" is reserved.', $this->expirationParameter));
62+
}
63+
64+
if (null !== $expiration) {
65+
$params[$this->expirationParameter] = $this->getExpirationTime($expiration);
66+
}
67+
4968
$uri = $this->buildUrl($url, $params);
5069
$params[$this->parameter] = $this->computeHash($uri);
5170

@@ -54,6 +73,7 @@ public function sign(string $uri): string
5473

5574
/**
5675
* Checks that a URI contains the correct hash.
76+
* Also checks if the URI has not expired (If you used expiration during signing).
5777
*/
5878
public function check(string $uri): bool
5979
{
@@ -71,7 +91,15 @@ public function check(string $uri): bool
7191
$hash = $params[$this->parameter];
7292
unset($params[$this->parameter]);
7393

74-
return hash_equals($this->computeHash($this->buildUrl($url, $params)), $hash);
94+
if (!hash_equals($this->computeHash($this->buildUrl($url, $params)), $hash)) {
95+
return false;
96+
}
97+
98+
if ($expiration = $params[$this->expirationParameter] ?? false) {
99+
return !$this->isExpired((int) $expiration);
100+
}
101+
102+
return true;
75103
}
76104

77105
public function checkRequest(Request $request): bool
@@ -104,6 +132,24 @@ private function buildUrl(array $url, array $params = []): string
104132

105133
return $scheme.$user.$pass.$host.$port.$path.$query.$fragment;
106134
}
135+
136+
private function getExpirationTime(\DateTimeInterface|\DateInterval|int $expiration): string
137+
{
138+
if ($expiration instanceof \DateTimeInterface) {
139+
return $expiration->format('U');
140+
}
141+
142+
if ($expiration instanceof \DateInterval) {
143+
return (new \DateTimeImmutable())->add($expiration)->format('U');
144+
}
145+
146+
return (string) $expiration;
147+
}
148+
149+
private function isExpired(int $expiration): bool
150+
{
151+
return time() >= $expiration;
152+
}
107153
}
108154

109155
if (!class_exists(\Symfony\Component\HttpKernel\UriSigner::class, false)) {

0 commit comments

Comments
 (0)
0