8000 rework CookieTokenStorage · symfony/symfony@6e19ef9 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6e19ef9

Browse files
committed
rework CookieTokenStorage
1 parent 61b5483 commit 6e19ef9

File tree

2 files changed

+108
-99
lines changed

2 files changed

+108
-99
lines changed

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

Lines changed: 107 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
namespace Symfony\Component\Security\Csrf\TokenStorage;
1313

1414
use Symfony\Component\HttpFoundation\Cookie;
15-
use Symfony\Component\HttpFoundation\ParameterBag;
1615
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
1716
use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException;
1817

@@ -31,22 +30,22 @@ class CookieTokenStorage implements TokenStorageInterface
3130
const COOKIE_DELIMITER = '_';
3231

3332
/**
34-
* @var array
33+
* @var array A map of tokens to be written in the response
3534
*/
3635
private $transientTokens = array();
3736

3837
/**
39-
* @var array
38+
* @var array A map of tokens extracted from cookies and verified
4039
*/
41-
private $resolvedTokens = array();
40+
private $extractedTokens = array();
4241

4342
/**
4443
* @var array
4544
*/
46-
private $refreshTokens = array();
45+
private $nonces = array();
4746

4847
/**
49-
* @var ParameterBag
48+
* @var array
5049
*/
5150
private $cookies;
5251

@@ -66,14 +65,14 @@ class CookieTokenStorage implements TokenStorageInterface
6665
private $ttl;
6766

6867
/**
69-
* @param ParameterBag $cookies
70-
* @param bool $secure
71-
* @param string $secret
72-
* @param int $ttl
68+
* @param string $cookies The raw HTTP Cookie header
69+
* @param bool $secure
70+
* @param string $secret
71+
* @param int $ttl
7372
*/
74-
public function __construct(ParameterBag $cookies, $secure, $secret, $ttl = null)
73+
public function __construct($cookies, $secure, $secret, $ttl = null)
7574
{
76-
$this->cookies = $cookies;
75+
$this->cookies = self::parseCookieHeader($cookies);
7776
$this->secure = (bool) $secure;
7877
$this->secret = (string) $secret;
7978
$this->ttl = $ttl === null ? 60 * 60 : (int) $ttl;
@@ -120,7 +119,10 @@ public function setToken($tokenId, $token)
120119
throw new InvalidArgumentException('Empty tokens are not allowed');
121120
}
122121

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

126128
/**
@@ -130,106 +132,119 @@ public function removeToken($tokenId)
130132
{
131133
$token = $this->resolveToken($tokenId);
132134

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

135137
return '' === $token ? null : $token;
136138
}
137139

138140
/**
139-
* @return array
141+
* @return Cookie[]
140142
*/
141143
public function createCookies()
142144
{
143145
$cookies = array();
144146

145147
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;
148+
if (isset($this->nonces[$tokenId])) {
149+
foreach (array_keys($this->nonces[$tokenId]) as $nonce) {
150+
$cookies[] = $this->createDeleteCookie($tokenId, $nonce);
151+
}
157152
}
158153

159-
$cookies[] = $this->createVerificationCookie($tokenId, $token);
154+
if ($token !== '') {
155+
$cookies[] = $this->createCookie($tokenId, $token);
156+
}
160157
}
161158

162159
return $cookies;
163160
}
164161

165162
/**
166163
* @param string $tokenId
167-
* @param bool $excludeTransient
168164
*
169165
* @return string
170166
*/
171-
protected function resolveToken($tokenId, $excludeTransient = false)
167+
protected function resolveToken($tokenId)
172168
{
173-
if (!$excludeTransient && isset($this->transientTokens[$tokenId])) {
169+
if (isset($this->transientTokens[$tokenId])) {
174170
return $this->transientTokens[$tokenId];
175171
}
176172

177-
if (isset($this->resolvedTokens[$tokenId])) {
178-
return $this->resolvedTokens[$tokenId];
173+
if (isset($this->extractedTokens[$tokenId])) {
174+
return $this->extractedTokens[$tokenId];
179175
}
180176

181-
$this->resolvedTokens[$tokenId] = '';
177+
$this->extractedTokens[$tokenId] = '';
182178

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

188-
$parts = explode(self::COOKIE_DELIMITER, $this->getVerificationCookieValue($tokenId), 2);
189-
if (count($parts) != 2) {
194+
$parts = explode(self::COOKIE_DELIMITER, $cookies[0][1], 3);
195+
if (count($parts) != 3) {
190196
return '';
191197
}
198+
list($expires, $signature, $token) = $parts;
192199

193-
list($expires, $hash) = $parts;
200+
// expired token
194201
$time = time();
195202
if (!ctype_digit($expires) || $expires < $time) {
196203
return '';
197204
}
198-
if (!hash_equals($this->generateVerificationHash($tokenId, $token, $expires), $hash)) {
205+
206+
// invalid signature
207+
$nonce = substr($cookies[0][0], $prefixLength);
208+
if (!hash_equals($this->generateSignature($tokenId, $token, $expires, $nonce), $signature)) {
199209
return '';
200210
}
201211

202212
$time += $this->ttl / 2;
203213
if ($expires < $time) {
204-
$this->refreshTokens[$tokenId] = $token;
214+
$this->transientTokens[$tokenId] = $token;
205215
}
206216

207-
return $this->resolvedTokens[$tokenId] = $token;
217+
return $this->extractedTokens[$tokenId] = $token;
208218
}
209219

210220
/**
211-
* @param string $tokenId
212-
* @param string $token
221+
* @param string $prefix
222+
*
223+
* @return array
213224
*/
214-
protected function updateToken($tokenId, $token)
225+
protected function findCookiesByPrefix($prefix)
215226
{
216-
if ($token === $this->resolveToken($tokenId, true)) {
217-
unset($this->transientTokens[$tokenId]);
218-
} else {
219-
$this->transientTokens[$tokenId] = $token;
227+
$cookies = array();
228+
foreach ($this->cookies as $cookie) {
229+
if (0 === strpos($cookie[0], $prefix)) {
230+
$cookies[] = $cookie;
231+
}
220232
}
233+
234+
return $cookies;
221235
}
222236

223237
/**
224238
* @param string $tokenId
239+
* @param string $nonce
225240
*
226-
* @return string
241+
* @return Cookie
227242
*/
228-
protected function getTokenCookieValue($tokenId)
243+
protected function createDeleteCookie($tokenId, $nonce)
229244
{
230-
$name = $this->generateTokenCookieName($tokenId);
245+
$name = $this->generateCookieName($tokenId, $nonce);
231246

232-
return $this->cookies->get($name, '');
247+
return new Cookie($name, '', 0, null, null, $this->secure, true);
233248
}
234249

235250
/**
@@ -238,88 +253,82 @@ protected function getTokenCookieValue($tokenId)
238253
*
239254
* @return Cookie
240255
*/
241-
protected function createTokenCookie($tokenId, $token)
256+
protected function createCookie($tokenId, $token)
242257
{
243-
$name = $this->generateTokenCookieName($tokenId);
258+
$expires = time() + $this->ttl;
259+
$nonce = self::encodeBase64Url(random_bytes(6));
260+
$signature = $this->generateSignature($tokenId, $token, $expires, $nonce);
244261

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

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

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

260270
/**
261271
* @param string $tokenId
272+
* @param string $nonce
262273
*
263274
* @return string
264275
*/
265-
protected function getVerificationCookieValue($tokenId)
276+
protected function generateCookieName($tokenId, $nonce)
266277
{
267-
$name = $this->generateVerificationCookieName($tokenId);
268-
269-
return $this->cookies->get($name, '');
278+
return sprintf(
279+
'_csrf_%s_%s_%s',
280+
(int) $this->secure,
281+
self::encodeBase64Url($tokenId),
282+
$nonce
283+
);
270284
}
271285

272286
/**
273287
* @param string $tokenId
274288
* @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
289+
* @param int $expires
290+
* @param string $nonce
288291
*
289292
* @return string
290293
*/
291-
protected function generateVerificationCookieName($tokenId)
294+
protected function generateSignature($tokenId, $token, $expires, $nonce)
292295
{
293-
return $this->generateTokenCookieName($tokenId).'/verify';
296+
return hash_hmac('sha256', $tokenId.$token.$expires.$nonce.$this->secure, $this->secret);
294297
}
295298

296299
/**
297-
* @param string $tokenId
298-
* @param string $token
300+
* @param string $header
299301
*
300-
* @return string
302+
* @return array
301303
*/
302-
protected function generateVerificationCookieValue($tokenId, $token)
304+
public static function parseCookieHeader($header)
303305
{
304-
if ('' === $token) {
305-
return '';
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

Lines 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