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 e3b363a

Browse filesBrowse files
committed
rework CookieTokenStorage
1 parent 61b5483 commit e3b363a
Copy full SHA for e3b363a

File tree

2 files changed

+109
-100
lines changed
Filter options

2 files changed

+109
-100
lines changed

‎src/Symfony/Component/Security/Csrf/TokenStorage/CookieTokenStorage.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Csrf/TokenStorage/CookieTokenStorage.php
+108-99Lines changed: 108 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\HttpFoundation\ParameterBag;
1616
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
1717
use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException;
18+
use Symfony\Component\HttpFoundation\Session\SessionInterface;
1819

1920
/**
2021
* Accesses tokens in a set of cookies. A changeset records edits made to
@@ -31,22 +32,22 @@ class CookieTokenStorage implements TokenStorageInterface
3132
const COOKIE_DELIMITER = '_';
3233

3334
/**
34-
* @var array
35+
* @var array A map of tokens to be written in the response
3536
*/
3637
private $transientTokens = array();
3738

3839
/**
39-
* @var array
40+
* @var array A map of tokens extracted from cookies and verified
4041
*/
41-
private $resolvedTokens = array();
42+
private $extractedTokens = array();
4243

4344
/**
4445
* @var array
4546
*/
46-
private $refreshTokens = array();
47+
private $nonces = array();
4748

4849
/**
49-
* @var ParameterBag
50+
* @var array
5051
*/
5152
private $cookies;
5253

@@ -66,14 +67,14 @@ class CookieTokenStorage implements TokenStorageInterface
6667
private $ttl;
6768

6869
/**
69-
* @param ParameterBag $cookies
70-
* @param bool $secure
71-
* @param string $secret
72-
* @param int $ttl
70+
* @param string $cookies The raw HTTP Cookie header
71+
* @param bool $secure
72+
* @param string $secret
73+
* @param int $ttl
7374
*/
74-
public function __construct(ParameterBag $cookies, $secure, $secret, $ttl = null)
75+
public function __construct($cookies, $secure, $secret, $ttl = null)
7576
{
76-
$this->cookies = $cookies;
77+
$this->cookies = self::parseCookieHeader($cookies);
7778
$this->secure = (bool) $secure;
7879
$this->secret = (string) $secret;
7980
$this->ttl = $ttl === null ? 60 * 60 : (int) $ttl;
@@ -120,7 +121,10 @@ public function setToken($tokenId, $token)
120121
throw new InvalidArgumentException('Empty tokens are not allowed');
121122
}
122123

123-
$this->updateToken($tokenId, $token);
124+
// we need to resolve the token first to record the nonces
125+
$this->resolveToken($tokenId);
126+
127+
$this->transientTokens[$tokenId] = $token;
124128
}
125129

126130
/**
@@ -130,106 +134,118 @@ public function removeToken($tokenId)
130134
{
131135
$token = $this->resolveToken($tokenId);
132136

133-
$this->updateToken($tokenId, '');
137+
$this->transientTokens[$tokenId] = '';
134138

135139
return '' === $token ? null : $token;
136140
}
137141

138142
/**
139-
* @return array
143+
* @return Cookie[]
140144
*/
141145
public function createCookies()
142146
{
143147
$cookies = array();
144148

145149
foreach ($this->transientTokens as $tokenId => $token) {
146-
// FIXME empty tokens are handled by the http foundations cookie class
147-
// and are recognized as a "delete" cookie
148-
// the problem is the that the value of deleted cookies get set to
149-
// the string "deleted" and not the empty string
150-
$cookies[] = $this->createTokenCookie($tokenId, $token);
151-
$cookies[] = $this->createVerificationCookie($tokenId, $token);
152-
}
153-
154-
foreach ($this->refreshTokens as $tokenId => $token) {
155-
if (isset($this->transientTokens[$tokenId])) {
156-
continue;
150+
if (isset($this->nonces[$tokenId])) {
151+
foreach(array_keys($this->nonces[$tokenId]) as $nonce) {
152+
$cookies[] = $this->createDeleteCookie($tokenId, $nonce);
153+
}
157154
}
158155

159-
$cookies[] = $this->createVerificationCookie($tokenId, $token);
156+
if ($token !== '') {
157+
$cookies[] = $this->createCookie($tokenId, $token);
158+
}
160159
}
161160

162161
return $cookies;
163162
}
164163

165164
/**
166165
* @param string $tokenId
167-
* @param bool $excludeTransient
168166
*
169167
* @return string
170168
*/
171-
protected function resolveToken($tokenId, $excludeTransient = false)
169+
protected function resolveToken($tokenId)
172170
{
173-
if (!$excludeTransient && isset($this->transientTokens[$tokenId])) {
171+
if (isset($this->transientTokens[$tokenId])) {
174172
return $this->transientTokens[$tokenId];
175173
}
176174

177-
if (isset($this->resolvedTokens[$tokenId])) {
178-
return $this->resolvedTokens[$tokenId];
175+
if (isset($this->extractedTokens[$tokenId])) {
176+
return $this->extractedTokens[$tokenId];
179177
}
180178

181-
$this->resolvedTokens[$tokenId] = '';
179+
$this->extractedTokens[$tokenId] = '';
182180

183-
$token = $this->getTokenCookieValue($tokenId);
184-
if ('' === $token) {
181+
$prefix = $this->generateCookieName($tokenId, '');
182+
$prefixLength = strlen($prefix);
183+
$cookies = $this->findCookiesByPrefix($prefix);
184+
185+
// record the nonces used, so we can delete all obsolete cookies of this
186+
// token id, if necessary
187+
foreach($cookies as $cookie) {
188+
$this->nonces[$tokenId][substr($cookie[0], $prefixLength)] = true;
189+
}
190+
191+
// if there is more than one cookie for the prefix, we get cookie tossed maybe
192+
if (count($cookies) != 1) {
185193
return '';
186194
}
187195

188-
$parts = explode(self::COOKIE_DELIMITER, $this->getVerificationCookieValue($tokenId), 2);
189-
if (count($parts) != 2) {
196+
$parts = explode(self::COOKIE_DELIMITER, $cookies[0][1], 3);
197+
if (count($parts) != 3) {
190198
return '';
191199
}
200+
list($expires, $signature, $token) = $parts;
192201

193-
list($expires, $hash) = $parts;
202+
// expired token
194203
$time = time();
195204
if (!ctype_digit($expires) || $expires < $time) {
196205
return '';
197206
}
198-
if (!hash_equals($this->generateVerificationHash($tokenId, $token, $expires), $hash)) {
207+
208+
// invalid signature
209+
$nonce = substr($cookies[0][0], $prefixLength);
210+
if (!hash_equals($this->generateSignature($tokenId, $token, $expires, $nonce), $signature)) {
199211
return '';
200212
}
201213

202214
$time += $this->ttl / 2;
203215
if ($expires < $time) {
204-
$this->refreshTokens[$tokenId] = $token;
216+
$this->transientTokens[$tokenId] = $token;
205217
}
206218

207-
return $this->resolvedTokens[$tokenId] = $token;
219+
return $this->extractedTokens[$tokenId] = $token;
208220
}
209221

210222
/**
211-
* @param string $tokenId
212-
* @param string $token
223+
* @param string $prefix
224+
*
225+
* @return array
213226
*/
214-
protected function updateToken($tokenId, $token)
215-
{
216-
if ($token === $this->resolveToken($tokenId, true)) {
217-
unset($this->transientTokens[$tokenId]);
218-
} else {
219-
$this->transientTokens[$tokenId] = $token;
227+
protected function findCookiesByPrefix($prefix) {
228+
$cookies = array();
229+
foreach ($this->cookies as $cookie) {
230+
if (0 === strpos($cookie[0], $prefix)) {
231+
$cookies[] = $cookie;
232+
}
220233
}
234+
235+
return $cookies;
221236
}
222237

223238
/**
224239
* @param string $tokenId
240+
* @param string $nonce
225241
*
226-
* @return string
242+
* @return Cookie
227243
*/
228-
protected function getTokenCookieValue($tokenId)
244+
protected function createDeleteCookie($tokenId, $nonce)
229245
{
230-
$name = $this->generateTokenCookieName($tokenId);
246+
$name = $this->generateCookieName($tokenId, $nonce);
231247

232-
return $this->cookies->get($name, '');
248+
return new Cookie($name, '', 0, null, null, $this->secure, true);
233249
}
234250

235251
/**
@@ -238,88 +254,81 @@ protected function getTokenCookieValue($tokenId)
238254
*
239255
* @return Cookie
240256
*/
241-
protected function createTokenCookie($tokenId, $token)
257+
protected function createCookie($tokenId, $token)
242258
{
243-
$name = $this->generateTokenCookieName($tokenId);
259+
$expires = time() + $this->ttl;
260+
$nonce = self::encodeBase64Url(random_bytes(6));
261+
$signature = $this->generateSignature($tokenId, $token, $expires, $nonce);
244262

245-
return new Cookie($name, $token, 0, null, null, $this->secure, false);
246-
}
263+
$this->nonces[$tokenId][$nonce] = true;
247264

248-
/**
249-
* @param string $tokenId
250-
*
251-
* @return string
252-
*/
253-
protected function generateTokenCookieName($tokenId)
254-
{
255-
$encodedTokenId = rtrim(strtr(base64_encode($tokenId), '+/', '-_'), '=');
265+
$name = $this->generateCookieName($tokenId, $nonce);
266+
$value = $expires.self::COOKIE_DELIMITER.$signature.self::COOKIE_DELIMITER.$token;
256267

257-
return sprintf('_csrf/%s/%s', $this->secure ? 'secure' : 'insecure', $encodedTokenId);
268+
return new Cookie($name, $value, 0, null, null, $this->secure, true);
258269
}
259270

260271
/**
261272
* @param string $tokenId
273+
* @param string $nonce
262274
*
263275
* @return string
264276
*/
265-
protected function getVerificationCookieValue($tokenId)
277+
protected function generateCookieName($tokenId, $nonce)
266278
{
267-
$name = $this->generateVerificationCookieName($tokenId);
268-
269-
return $this->cookies->get($name, '');
279+
return sprintf(
280+
'_csrf_%s_%s_%s',
281+
(int) $this->secure,
282+
self::encodeBase64Url($tokenId),
283+
$nonce
284+
);
270285
}
271286

272287
/**
273288
* @param string $tokenId
274289
* @param string $token
275-
*
276-
* @return Cookie
277-
*/
278-
protected function createVerificationCookie($tokenId, $token)
279-
{
280-
$name = $this->generateVerificationCookieName($tokenId);
281-
$value = $this->generateVerificationCookieValue($tokenId, $token);
282-
283-
return new Cookie($name, $value, 0, null, null, $this->secure, true);
284-
}
285-
286-
/**
287-
* @param string $tokenId
290+
* @param int $expires
291+
* @param string $nonce
288292
*
289293
* @return string
290294
*/
291-
protected function generateVerificationCookieName($tokenId)
295+
protected function generateSignature($tokenId, $token, $expires, $nonce)
292296
{
293-
return $this->generateTokenCookieName($tokenId).'/verify';
297+
return hash_hmac('sha256', $tokenId.$token.$expires.$nonce.$this->secure, $this->secret);
294298
}
295299

296300
/**
297-
* @param string $tokenId
298-
* @param string $token
301+
* @param string $header
299302
*
300-
* @return string
303+
* @return array
301304
*/
302-
protected function generateVerificationCookieValue($tokenId, $token)
303-
{
304-
if ('' === $token) {
305-
return '';
305+
public static function parseCookieHeader($header) {
306+
$header = trim((string) $header);
307+
if ('' === $header) {
308+
return array();
306309
}
307310

308-
$expires = time() + $this->ttl;
309-
$hash = $this->generateVerificationHash($tokenId, $token, $expires);
311+
$cookies = array();
312+
foreach(explode(';', $header) as $cookie) {
313+
if (false === strpos($cookie, '=')) {
314+
continue;
315+
}
316+
317+
$cookies[] = array_map(function($item) {
318+
return urldecode(trim($item, ' "'));
319+
}, explode('=', $cookie, 2));
320+
}
310321

311-
return $expires.self::COOKIE_DELIMITER.$hash;
322+
return $cookies;
312323
}
313324

314325
/**
315-
* @param string $tokenId
316-
* @param string $token
317-
* @param int $expires
326+
* @param string $data
318327
*
319328
* @return string
320329
*/
321-
protected function generateVerificationHash($tokenId, $token, $expires)
330+
public static function encodeBase64Url($data)
322331
{
323-
return hash_hmac('sha256', $tokenId.$token.$expires, $this->secret);
332+
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
324333
}
325334
}

‎src/Symfony/Component/Security/Csrf/TokenStorage/CookieTokenStorageFactory.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Csrf/TokenStorage/CookieTokenStorageFactory.php
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,6 @@ public function __construct($secret, $ttl = null)
5454
*/
5555
public function createTokenStorage(Request $request)
5656
{
57-
return new CookieTokenStorage($request->cookies, $request->isSecure(), $this->secret, $this->ttl);
57+
return new CookieTokenStorage($request->headers->get('Cookie'), $request->isSecure(), $this->secret, $this->ttl);
5858
}
5959
}

0 commit comments

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