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

Skip to content

Commit 391443c

Browse files
[HttpFoundation] Add temporary URI signed
1 parent c71348a commit 391443c

File tree

3 files changed

+154
-5
lines changed

3 files changed

+154
-5
lines changed

src/Symfony/Component/HttpFoundation/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ CHANGELOG
44
7.1
55
---
66

7+
* Add optional `$expirationParameter` argument to `UriSigner::__construct()`
8+
* Add optional `$expiration` argument to `UriSigner::sign()`
79
* Add `UploadedFile::getClientOriginalPath()`
810

911
7.0

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

+107-1
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));
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,79 @@ 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 testSignWithoutExpirationAndWithReservedHashParameter()
136+
{
137+
$signer = new UriSigner('foobar');
138+
139+
$this->expectException(\LogicException::class);
140+
141+
$signer->sign('http://example.com/foo?_hash=bar');
142+
}
143+
144+
public function testSignWithoutExpirationAndWithReservedParameter()
145+
{
146+
$signer = new UriSigner('foobar');
147+
148+
$this->expectException(\LogicException::class);
149+
150+
$signer->sign('http://example.com/foo?_expiration=4070908800');
151+
}
152+
153+
public function testSignWithExpirationAndWithReservedHashParameter()
154+
{
155+
$signer = new UriSigner('foobar');
156+
157+
$this->expectException(\LogicException::class);
158+
159+
$signer->sign('http://example.com/foo?_hash=bar', new \DateTimeImmutable('2099-01-01 00:00:00'));
160+
}
161+
162+
public function testSignWithExpirationAndWithReservedParameter()
163+
{
164+
$signer = new UriSigner('foobar');
165+
166+
$this->expectException(\LogicException::class);
167+
168+
$signer->sign('http://example.com/foo?_expiration=4070908800', new \DateTimeImmutable('2099-01-01 00:00:00'));
169+
}
170+
171+
public function testCheckWithUriExpiration()
172+
{
173+
$signer = new UriSigner('foobar');
174+
175+
$this->assertFalse($signer->check($signer->sign('http://example.com/foo', new \DateTimeImmutable('2000-01-01 00:00:00'))));
176+
$this->assertFalse($signer->check($signer->sign('http://example.com/foo?foo=bar', new \DateTimeImmutable('2000-01-01 00:00:00'))));
177+
$this->assertFalse($signer->check($signer->sign('http://example.com/foo?foo=bar&0=integer', new \DateTimeImmutable('2000-01-01 00:00:00'))));
178+
179+
$this->assertFalse($signer->check($signer->sign('http://example.com/foo', 1577836800))); // 2000-01-01
180+
$this->assertFalse($signer->check($signer->sign('http://example.com/foo?foo=bar', 1577836800))); // 2000-01-01
181+
$this->assertFalse($signer->check($signer->sign('http://example.com/foo?foo=bar&0=integer', 1577836800))); // 2000-01-01
182+
183+
$relativeUriFromNow1 = $signer->sign('http://example.com/foo', new \DateInterval('PT3S'));
184+
$relativeUriFromNow2 = $signer->sign('http://example.com/foo?foo=bar', new \DateInterval('PT3S'));
185+
$relativeUriFromNow3 = $signer->sign('http://example.com/foo?foo=bar&0=integer', new \DateInterval('PT3S'));
186+
sleep(10);
187+
188+
$this->assertFalse($signer->check($relativeUriFromNow1));
189+
$this->assertFalse($signer->check($relativeUriFromNow2));
190+
$this->assertFalse($signer->check($relativeUriFromNow3));
85191
}
86192
}

src/Symfony/Component/HttpFoundation/UriSigner.php

+45-4
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,38 @@ class UriSigner
1818
{
1919
private string $secret;
2020
private string $parameter;
21+
private string $expirationParameter;
2122

2223
/**
23-
* @param string $parameter Query string parameter to use
24+
* @param string $parameter Query string parameter to use
25+
* @param string $expirationParameter Query string parameter to use for expiration
2426
*/
25-
public function __construct(#[\SensitiveParameter] string $secret, string $parameter = '_hash')
27+
public function __construct(#[\SensitiveParameter] string $secret, string $parameter = '_hash', string $expirationParameter = '_expiration')
2628
{
2729
if (!$secret) {
2830
throw new \InvalidArgumentException('A non-empty secret is required.');
2931
}
3032

3133
$this->secret = $secret;
3234
$this->parameter = $parameter;
35+
$this->expirationParameter = $expirationParameter;
3336
}
3437

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

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

@@ -55,6 +74,7 @@ public function sign(string $uri): string
5574

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

75-
return hash_equals($this->computeHash($this->buildUrl($url, $params)), $hash);
95+
if (!hash_equals($this->computeHash($this->buildUrl($url, $params)), $hash)) {
96+
return false;
97+
}
98+
99+
if ($expiration = $params[$this->expirationParameter] ?? false) {
100+
return time() < $expiration;
101+
}
102+
103+
return true;
76104
}
77105

78106
public function checkRequest(Request $request): bool
@@ -105,4 +133,17 @@ private function buildUrl(array $url, array $params = []): string
105133

106134
return $scheme.$user.$pass.$host.$port.$path.$query.$fragment;
107135
}
136+
137+
private function getExpirationTime(\DateTimeInterface|\DateInterval|int $expiration): string
138+
{
139+
if ($expiration instanceof \DateTimeInterface) {
140+
return $expiration->format('U');
141+
}
142+
143+
if ($expiration instanceof \DateInterval) {
144+
return \DateTimeImmutable::createFromFormat('U', time())->add($expiration)->format('U');
145+
}
146+
147+
return (string) $expiration;
148+
}
108149
}

0 commit comments

Comments
 (0)
0