@@ -54,7 +54,7 @@ class Security implements SecurityInterface
54
54
protected $ tokenRandomize = false ;
55
55
56
56
/**
57
- * CSRF Hash
57
+ * CSRF Hash (without randomization)
58
58
*
59
59
* Random hash for Cross Site Request Forgery protection.
60
60
*
@@ -88,7 +88,7 @@ class Security implements SecurityInterface
88
88
protected $ cookie ;
89
89
90
90
/**
91
- * CSRF Cookie Name
91
+ * CSRF Cookie Name (with Prefix)
92
92
*
93
93
* Cookie name for Cross Site Request Forgery protection.
94
94
*
@@ -154,6 +154,14 @@ class Security implements SecurityInterface
154
154
*/
155
155
private ?Session $ session = null ;
156
156
157
+ /**
158
+ * CSRF Hash in Request Cookie
159
+ *
160
+ * The cookie value is always CSRF hash (without randomization) even if
161
+ * $tokenRandomize is true.
162
+ */
163
+ private ?string $ hashInCookie = null ;
164
+
157
165
/**
158
166
* Constructor.
159
167
*
@@ -192,9 +200,13 @@ public function __construct(App $config)
192
200
$ this ->configureSession ();
193
201
}
194
202
195
- $ this ->request = Services::request ();
203
+ $ this ->request = Services::request ();
204
+ $ this ->hashInCookie = $ this ->request ->getCookie ($ this ->cookieName );
196
205
197
- $ this ->generateHash ();
206
+ $ this ->restoreHash ();
207
+ if ($ this ->hash === null ) {
208
+ $ this ->generateHash ();
209
+ }
198
210
}
199
211
200
212
private function isCSRFCookie (): bool
@@ -240,7 +252,7 @@ public function CSRFVerify(RequestInterface $request)
240
252
}
241
253
242
254
/**
243
- * Returns the CSRF Hash .
255
+ * Returns the CSRF Token .
244
256
*
245
257
* @deprecated Use `CodeIgniter\Security\Security::getHash()` instead of using this method.
246
258
*
@@ -293,6 +305,22 @@ public function verify(RequestInterface $request)
293
305
throw SecurityException::forDisallowedAction ();
294
306
}
295
307
308
+ $ this ->removeTokenInRequest ($ request );
309
+
310
+ if ($ this ->regenerate ) {
311
+ $ this ->generateHash ();
312
+ }
313
+
314
+ log_message ('info ' , 'CSRF token verified. ' );
315
+
316
+ return $ this ;
317
+ }
318
+
319
+ /**
320
+ * Remove token in POST or JSON request data
321
+ */
322
+ private function removeTokenInRequest (RequestInterface $ request ): void
323
+ {
296
324
$ json = json_decode ($ request ->getBody () ?? '' );
297
325
298
326
if (isset ($ _POST [$ this ->tokenName ])) {
@@ -304,22 +332,6 @@ public function verify(RequestInterface $request)
304
332
unset($ json ->{$ this ->tokenName });
305
333
$ request ->setBody (json_encode ($ json ));
306
334
}
307
-
308
- if ($ this ->regenerate ) {
309
- $ this ->hash = null ;
310
- if ($ this ->isCSRFCookie ()) {
311
- unset($ _COOKIE [$ this ->cookieName ]);
312
- } else {
313
- // Session based CSRF protection
314
- $ this ->session ->remove ($ this ->tokenName );
315
- }
316
- }
317
-
318
- $ this ->generateHash ();
319
-
320
- log_message ('info ' , 'CSRF token verified. ' );
321
-
322
- return $ this ;
323
335
}
324
336
325
337
private function getPostedToken (RequestInterface $ request ): ?string
@@ -342,7 +354,7 @@ private function getPostedToken(RequestInterface $request): ?string
342
354
}
343
355
344
356
/**
345
- * Returns the CSRF Hash .
357
+ * Returns the CSRF Token .
346
358
*/
347
359
public function getHash (): ?string
348
360
{
@@ -351,6 +363,10 @@ public function getHash(): ?string
351
363
352
364
/**
353
365
* Randomize hash to avoid BREACH attacks.
366
+ *
367
+ * @params string $hash CSRF hash
368
+ *
369
+ * @return string CSRF token
354
370
*/
355
371
protected function randomize (string $ hash ): string
356
372
{
@@ -367,7 +383,11 @@ protected function randomize(string $hash): string
367
383
/**
368
384
* Derandomize the token.
369
385
*
386
+ * @params string $token CSRF token
387
+ *
370
388
* @throws InvalidArgumentException "hex2bin(): Hexadecimal input string must have an even length"
389
+ *
390
+ * @return string CSRF hash
371
391
*/
372
392
protected function derandomize (string $ token ): string
373
393
{
@@ -493,42 +513,47 @@ public function sanitizeFilename(string $str, bool $relativePath = false): strin
493
513
}
494
514
495
515
/**
496
- * Generates the CSRF Hash.
516
+ * Restore hash from Session or Cookie
497
517
*/
498
- protected function generateHash (): string
518
+ private function restoreHash (): void
499
519
{
500
- if ($ this ->hash === null ) {
501
- // If the cookie exists we will use its value.
502
- // We don't necessarily want to regenerate it with
503
- // each page load since a page could contain embedded
504
- // sub-pages causing this feature to fail
505
- if ($ this ->isCSRFCookie ()) {
506
- if ($ this ->isHashInCookie ()) {
507
- return $ this ->hash = $ _COOKIE [$ this ->cookieName ];
508
- }
509
- } elseif ($ this ->session ->has ($ this ->tokenName )) {
510
- // Session based CSRF protection
511
- return $ this ->hash = $ this ->session ->get ($ this ->tokenName );
520
+ if ($ this ->isCSRFCookie ()) {
521
+ if ($ this ->isHashInCookie ()) {
522
+ $ this ->hash = $ this ->hashInCookie ;
512
523
}
524
+ } elseif ($ this ->session ->has ($ this ->tokenName )) {
525
+ // Session based CSRF protection
526
+ $ this ->hash = $ this ->session ->get ($ this ->tokenName );
527
+ }
528
+ }
513
529
514
- $ this ->hash = bin2hex (random_bytes (static ::CSRF_HASH_BYTES ));
530
+ /**
531
+ * Generates (Regenerate) the CSRF Hash.
532
+ */
533
+ protected function generateHash (): string
534
+ {
535
+ $ this ->hash = bin2hex (random_bytes (static ::CSRF_HASH_BYTES ));
515
536
516
- if ($ this ->isCSRFCookie ()) {
517
- $ this ->saveHashInCookie ();
518
- } else {
519
- // Session based CSRF protection
520
- $ this ->saveHashInSession ();
521
- }
537
+ if ($ this ->isCSRFCookie ()) {
538
+ $ this ->saveHashInCookie ();
539
+ } else {
540
+ // Session based CSRF protection
541
+ $ this ->saveHashInSession ();
522
542
}
523
543
524
544
return $ this ->hash ;
525
545
}
526
546
527
547
private function isHashInCookie (): bool
528
548
{
529
- return isset ($ _COOKIE [$ this ->cookieName ])
530
- && is_string ($ _COOKIE [$ this ->cookieName ])
531
- && preg_match ('#^[0-9a-f]{32}$#iS ' , $ _COOKIE [$ this ->cookieName ]) === 1 ;
549
+ if ($ this ->hashInCookie === null ) {
550
+ return false ;
551
+ }
552
+
553
+ $ length = static ::CSRF_HASH_BYTES * 2 ;
554
+ $ pattern = '#^[0-9a-f]{ ' . $ length . '}$#iS ' ;
555
+
556
+ return preg_match ($ pattern , $ this ->hashInCookie ) === 1 ;
532
557
}
533
558
534
559
private function saveHashInCookie (): void
0 commit comments