2121use CodeIgniter \HTTP \RequestInterface ;
2222use CodeIgniter \I18n \Time ;
2323use CodeIgniter \Security \Exceptions \SecurityException ;
24- use CodeIgniter \Session \Session ;
24+ use CodeIgniter \Session \SessionInterface ;
2525use Config \Cookie as CookieConfig ;
2626use Config \Security as SecurityConfig ;
27- use ErrorException ;
2827use JsonException ;
2928use SensitiveParameter ;
3029
@@ -38,7 +37,16 @@ class Security implements SecurityInterface
3837{
3938 public const CSRF_PROTECTION_COOKIE = 'cookie ' ;
4039 public const CSRF_PROTECTION_SESSION = 'session ' ;
41- protected const CSRF_HASH_BYTES = 16 ;
40+
41+ /**
42+ * CSRF hash length in bytes.
43+ */
44+ protected const CSRF_HASH_BYTES = 16 ;
45+
46+ /**
47+ * CSRF hash length in hexadecimal characters.
48+ */
49+ protected const CSRF_HASH_HEX = self ::CSRF_HASH_BYTES * 2 ;
4250
4351 /**
4452 * CSRF Hash (without randomization)
@@ -51,6 +59,8 @@ class Security implements SecurityInterface
5159
5260 /**
5361 * @var Cookie
62+ *
63+ * @deprecated v4.8.0 Use service('response')->getCookie() instead.
5464 */
5565 protected $ cookie ;
5666
@@ -63,17 +73,12 @@ class Security implements SecurityInterface
6373 */
6474 protected $ cookieName = 'csrf_cookie_name ' ;
6575
66- private readonly IncomingRequest $ request ;
67-
6876 /**
6977 * CSRF Cookie Name without Prefix
7078 */
7179 private ?string $ rawCookieName = null ;
7280
73- /**
74- * Session instance.
75- */
76- private ?Session $ session = null ;
81+ private ?SessionInterface $ session = null ;
7782
7883 /**
7984 * CSRF Hash in Request Cookie
@@ -83,25 +88,17 @@ class Security implements SecurityInterface
8388 */
8489 private ?string $ hashInCookie = null ;
8590
86- protected SecurityConfig $ config ;
87-
88- public function __construct (SecurityConfig $ config )
91+ public function __construct (protected SecurityConfig $ config )
8992 {
90- $ this ->config = $ config ;
91-
9293 $ this ->rawCookieName = $ config ->cookieName ;
9394
94- if ($ this ->isCSRFCookie ()) {
95- $ cookie = config (CookieConfig::class);
96-
97- $ this ->configureCookie ($ cookie );
95+ if ($ this ->isCsrfCookie ()) {
96+ $ this ->configureCookie (config (CookieConfig::class));
9897 } else {
99- // Session based CSRF protection
10098 $ this ->configureSession ();
10199 }
102100
103- $ this ->request = service ('request ' );
104- $ this ->hashInCookie = $ this ->request ->getCookie ($ this ->cookieName );
101+ $ this ->hashInCookie = service ('request ' )->getCookie ($ this ->cookieName );
105102
106103 $ this ->restoreHash ();
107104
@@ -148,7 +145,9 @@ public function verify(RequestInterface $request): static
148145
149146 public function getHash (): ?string
150147 {
151- return $ this ->config ->tokenRandomize ? $ this ->randomize ($ this ->hash ) : $ this ->hash ;
148+ return $ this ->config ->tokenRandomize && isset ($ this ->hash )
149+ ? $ this ->randomize ($ this ->hash )
150+ : $ this ->hash ;
152151 }
153152
154153 public function getTokenName (): string
@@ -171,14 +170,16 @@ public function shouldRedirect(): bool
171170 return $ this ->config ->redirect ;
172171 }
173172
173+ /**
174+ * @phpstan-assert string $this->hash
175+ */
174176 public function generateHash (): string
175177 {
176178 $ this ->hash = bin2hex (random_bytes (static ::CSRF_HASH_BYTES ));
177179
178- if ($ this ->isCSRFCookie ()) {
180+ if ($ this ->isCsrfCookie ()) {
179181 $ this ->saveHashInCookie ();
180182 } else {
181- // Session based CSRF protection
182183 $ this ->saveHashInSession ();
183184 }
184185
@@ -207,30 +208,39 @@ protected function randomize(string $hash): string
207208 */
208209 protected function derandomize (#[SensitiveParameter] string $ token ): string
209210 {
210- $ key = substr ($ token , -static ::CSRF_HASH_BYTES * 2 );
211- $ value = substr ($ token , 0 , static ::CSRF_HASH_BYTES * 2 );
211+ // The token should be in the format of `randomizedHash` + `key`,
212+ // where both `randomizedHash` and `key` are hex strings of length CSRF_HASH_HEX.
213+ if (strlen ($ token ) !== self ::CSRF_HASH_HEX * 2 ) {
214+ throw new InvalidArgumentException ('Invalid CSRF token. ' );
215+ }
212216
213- try {
214- return bin2hex ((string ) hex2bin ($ value ) ^ (string ) hex2bin ($ key ));
215- } catch (ErrorException $ e ) {
216- throw new InvalidArgumentException ($ e ->getMessage (), $ e ->getCode (), $ e );
217+ $ keyBinary = hex2bin (substr ($ token , -self ::CSRF_HASH_HEX ));
218+ $ hashBinary = hex2bin (substr ($ token , 0 , self ::CSRF_HASH_HEX ));
219+
220+ if ($ hashBinary === false || $ keyBinary === false ) {
221+ throw new InvalidArgumentException ('Invalid CSRF token. ' );
217222 }
223+
224+ return bin2hex ($ hashBinary ^ $ keyBinary );
218225 }
219226
220- private function isCSRFCookie (): bool
227+ private function isCsrfCookie (): bool
221228 {
222229 return $ this ->config ->csrfProtection === self ::CSRF_PROTECTION_COOKIE ;
223230 }
224231
232+ /**
233+ * @phpstan-assert SessionInterface $this->session
234+ */
225235 private function configureSession (): void
226236 {
227237 $ this ->session = service ('session ' );
228238 }
229239
230240 private function configureCookie (CookieConfig $ cookie ): void
231241 {
232- $ cookiePrefix = $ cookie ->prefix ;
233- $ this -> cookieName = $ cookiePrefix . $ this -> rawCookieName ;
242+ $ this -> cookieName = $ cookie ->prefix . $ this -> rawCookieName ;
243+
234244 Cookie::setDefaults ($ cookie );
235245 }
236246
@@ -343,13 +353,16 @@ private function isNonEmptyTokenString(mixed $token): bool
343353 */
344354 private function restoreHash (): void
345355 {
346- if ($ this ->isCSRFCookie ()) {
347- if ($ this ->isHashInCookie ()) {
348- $ this ->hash = $ this ->hashInCookie ;
349- }
350- } elseif ($ this ->session ->has ($ this ->config ->tokenName )) {
351- // Session based CSRF protection
352- $ this ->hash = $ this ->session ->get ($ this ->config ->tokenName );
356+ if ($ this ->isCsrfCookie ()) {
357+ $ this ->hash = $ this ->isHashInCookie () ? $ this ->hashInCookie : null ;
358+
359+ return ;
360+ }
361+
362+ $ tokenName = $ this ->config ->tokenName ;
363+
364+ if ($ this ->session instanceof SessionInterface && $ this ->session ->has ($ tokenName )) {
365+ $ this ->hash = $ this ->session ->get ($ tokenName );
353366 }
354367 }
355368
@@ -359,28 +372,33 @@ private function isHashInCookie(): bool
359372 return false ;
360373 }
361374
362- $ length = static ::CSRF_HASH_BYTES * 2 ;
363- $ pattern = '#^[0-9a-f]{ ' . $ length . '}$#iS ' ;
375+ if (strlen ($ this ->hashInCookie ) !== self ::CSRF_HASH_HEX ) {
376+ return false ;
377+ }
364378
365- return preg_match ( $ pattern , $ this ->hashInCookie ) === 1 ;
379+ return ctype_xdigit ( $ this ->hashInCookie );
366380 }
367381
368382 private function saveHashInCookie (): void
369383 {
370- $ this ->cookie = new Cookie (
384+ $ expires = $ this ->config ->expires === 0 ? 0 : Time::now ()->getTimestamp () + $ this ->config ->expires ;
385+
386+ $ cookie = new Cookie (
371387 $ this ->rawCookieName ,
372388 $ this ->hash ,
373- [
374- 'expires ' => $ this ->config ->expires === 0 ? 0 : Time::now ()->getTimestamp () + $ this ->config ->expires ,
375- ],
389+ compact ('expires ' ),
376390 );
377391
378- $ response = service ('response ' );
379- $ response ->setCookie ($ this ->cookie );
392+ service ('response ' )->setCookie ($ cookie );
393+
394+ // For backward compatibility, we also set the cookie value to $this->cookie property.
395+ // @todo v4.8.0 Remove $this->cookie property and its usages.
396+ $ this ->cookie = $ cookie ;
380397 }
381398
382399 private function saveHashInSession (): void
383400 {
401+ assert ($ this ->session instanceof SessionInterface);
384402 $ this ->session ->set ($ this ->config ->tokenName , $ this ->hash );
385403 }
386404}
0 commit comments