• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 7123528876

07 Dec 2023 03:49AM UTC coverage: 84.987% (+0.02%) from 84.97%
7123528876

push

github

kenjis
Merge remote-tracking branch 'upstream/develop' into 4.5

12 of 12 new or added lines in 5 files covered. (100.0%)

1 existing line in 1 file now uncovered.

19236 of 22634 relevant lines covered (84.99%)

193.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.28
/system/Security/Security.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Security;
15

16
use CodeIgniter\Cookie\Cookie;
17
use CodeIgniter\HTTP\IncomingRequest;
18
use CodeIgniter\HTTP\Method;
19
use CodeIgniter\HTTP\Request;
20
use CodeIgniter\HTTP\RequestInterface;
21
use CodeIgniter\I18n\Time;
22
use CodeIgniter\Security\Exceptions\SecurityException;
23
use CodeIgniter\Session\Session;
24
use Config\Cookie as CookieConfig;
25
use Config\Security as SecurityConfig;
26
use Config\Services;
27
use ErrorException;
28
use InvalidArgumentException;
29
use LogicException;
30

31
/**
32
 * Class Security
33
 *
34
 * Provides methods that help protect your site against
35
 * Cross-Site Request Forgery attacks.
36
 *
37
 * @see \CodeIgniter\Security\SecurityTest
38
 */
39
class Security implements SecurityInterface
40
{
41
    public const CSRF_PROTECTION_COOKIE  = 'cookie';
42
    public const CSRF_PROTECTION_SESSION = 'session';
43
    protected const CSRF_HASH_BYTES      = 16;
44

45
    /**
46
     * CSRF Protection Method
47
     *
48
     * Protection Method for Cross Site Request Forgery protection.
49
     *
50
     * @var string 'cookie' or 'session'
51
     *
52
     * @deprecated 4.4.0 Use $this->config->csrfProtection.
53
     */
54
    protected $csrfProtection = self::CSRF_PROTECTION_COOKIE;
55

56
    /**
57
     * CSRF Token Randomization
58
     *
59
     * @var bool
60
     *
61
     * @deprecated 4.4.0 Use $this->config->tokenRandomize.
62
     */
63
    protected $tokenRandomize = false;
64

65
    /**
66
     * CSRF Hash (without randomization)
67
     *
68
     * Random hash for Cross Site Request Forgery protection.
69
     *
70
     * @var string|null
71
     */
72
    protected $hash;
73

74
    /**
75
     * CSRF Token Name
76
     *
77
     * Token name for Cross Site Request Forgery protection.
78
     *
79
     * @var string
80
     *
81
     * @deprecated 4.4.0 Use $this->config->tokenName.
82
     */
83
    protected $tokenName = 'csrf_token_name';
84

85
    /**
86
     * CSRF Header Name
87
     *
88
     * Header name for Cross Site Request Forgery protection.
89
     *
90
     * @var string
91
     *
92
     * @deprecated 4.4.0 Use $this->config->headerName.
93
     */
94
    protected $headerName = 'X-CSRF-TOKEN';
95

96
    /**
97
     * The CSRF Cookie instance.
98
     *
99
     * @var Cookie
100
     */
101
    protected $cookie;
102

103
    /**
104
     * CSRF Cookie Name (with Prefix)
105
     *
106
     * Cookie name for Cross Site Request Forgery protection.
107
     *
108
     * @var string
109
     */
110
    protected $cookieName = 'csrf_cookie_name';
111

112
    /**
113
     * CSRF Expires
114
     *
115
     * Expiration time for Cross Site Request Forgery protection cookie.
116
     *
117
     * Defaults to two hours (in seconds).
118
     *
119
     * @var int
120
     *
121
     * @deprecated 4.4.0 Use $this->config->expires.
122
     */
123
    protected $expires = 7200;
124

125
    /**
126
     * CSRF Regenerate
127
     *
128
     * Regenerate CSRF Token on every request.
129
     *
130
     * @var bool
131
     *
132
     * @deprecated 4.4.0 Use $this->config->regenerate.
133
     */
134
    protected $regenerate = true;
135

136
    /**
137
     * CSRF Redirect
138
     *
139
     * Redirect to previous page with error on failure.
140
     *
141
     * @var bool
142
     *
143
     * @deprecated 4.4.0 Use $this->config->redirect.
144
     */
145
    protected $redirect = false;
146

147
    /**
148
     * CSRF SameSite
149
     *
150
     * Setting for CSRF SameSite cookie token.
151
     *
152
     * Allowed values are: None - Lax - Strict - ''.
153
     *
154
     * Defaults to `Lax` as recommended in this link:
155
     *
156
     * @see https://portswigger.net/web-security/csrf/samesite-cookies
157
     *
158
     * @var string
159
     *
160
     * @deprecated `Config\Cookie` $samesite property is used.
161
     */
162
    protected $samesite = Cookie::SAMESITE_LAX;
163

164
    private IncomingRequest $request;
165

166
    /**
167
     * CSRF Cookie Name without Prefix
168
     */
169
    private ?string $rawCookieName = null;
170

171
    /**
172
     * Session instance.
173
     */
174
    private ?Session $session = null;
175

176
    /**
177
     * CSRF Hash in Request Cookie
178
     *
179
     * The cookie value is always CSRF hash (without randomization) even if
180
     * $tokenRandomize is true.
181
     */
182
    private ?string $hashInCookie = null;
183

184
    /**
185
     * Security Config
186
     */
187
    protected SecurityConfig $config;
188

189
    /**
190
     * Constructor.
191
     *
192
     * Stores our configuration and fires off the init() method to setup
193
     * initial state.
194
     */
195
    public function __construct(SecurityConfig $config)
196
    {
197
        $this->config = $config;
94✔
198

199
        $this->rawCookieName = $config->cookieName;
94✔
200

201
        if ($this->isCSRFCookie()) {
94✔
202
            $cookie = config(CookieConfig::class);
68✔
203

204
            $this->configureCookie($cookie);
68✔
205
        } else {
206
            // Session based CSRF protection
207
            $this->configureSession();
26✔
208
        }
209

210
        $this->request      = Services::request();
94✔
211
        $this->hashInCookie = $this->request->getCookie($this->cookieName);
94✔
212

213
        $this->restoreHash();
94✔
214
        if ($this->hash === null) {
94✔
215
            $this->generateHash();
54✔
216
        }
217
    }
218

219
    private function isCSRFCookie(): bool
220
    {
221
        return $this->config->csrfProtection === self::CSRF_PROTECTION_COOKIE;
94✔
222
    }
223

224
    private function configureSession(): void
225
    {
226
        $this->session = Services::session();
26✔
227
    }
228

229
    private function configureCookie(CookieConfig $cookie): void
230
    {
231
        $cookiePrefix     = $cookie->prefix;
68✔
232
        $this->cookieName = $cookiePrefix . $this->rawCookieName;
68✔
233
        Cookie::setDefaults($cookie);
68✔
234
    }
235

236
    /**
237
     * CSRF Verify
238
     *
239
     * @return $this
240
     *
241
     * @throws SecurityException
242
     */
243
    public function verify(RequestInterface $request)
244
    {
245
        // Protects POST, PUT, DELETE, PATCH
246
        $method           = $request->getMethod();
39✔
247
        $methodsToProtect = [Method::POST, Method::PUT, Method::DELETE, Method::PATCH];
39✔
248
        if (! in_array($method, $methodsToProtect, true)) {
39✔
249
            return $this;
3✔
250
        }
251

252
        $postedToken = $this->getPostedToken($request);
36✔
253

254
        try {
255
            $token = ($postedToken !== null && $this->config->tokenRandomize)
36✔
256
                ? $this->derandomize($postedToken) : $postedToken;
36✔
257
        } catch (InvalidArgumentException $e) {
1✔
258
            $token = null;
1✔
259
        }
260

261
        // Do the tokens match?
262
        if (! isset($token, $this->hash) || ! hash_equals($this->hash, $token)) {
36✔
263
            throw SecurityException::forDisallowedAction();
14✔
264
        }
265

266
        $this->removeTokenInRequest($request);
22✔
267

268
        if ($this->config->regenerate) {
22✔
269
            $this->generateHash();
18✔
270
        }
271

272
        log_message('info', 'CSRF token verified.');
22✔
273

274
        return $this;
22✔
275
    }
276

277
    /**
278
     * Remove token in POST or JSON request data
279
     */
280
    private function removeTokenInRequest(RequestInterface $request): void
281
    {
282
        assert($request instanceof Request);
283

284
        if (isset($_POST[$this->config->tokenName])) {
22✔
285
            // We kill this since we're done and we don't want to pollute the POST array.
286
            unset($_POST[$this->config->tokenName]);
11✔
287
            $request->setGlobal('post', $_POST);
11✔
288
        } else {
289
            $body = $request->getBody() ?? '';
11✔
290
            $json = json_decode($body);
11✔
291
            if ($json !== null && json_last_error() === JSON_ERROR_NONE) {
11✔
292
                // We kill this since we're done and we don't want to pollute the JSON data.
293
                unset($json->{$this->config->tokenName});
3✔
294
                $request->setBody(json_encode($json));
3✔
295
            } else {
296
                parse_str($body, $parsed);
8✔
297
                // We kill this since we're done and we don't want to pollute the BODY data.
298
                unset($parsed[$this->config->tokenName]);
8✔
299
                $request->setBody(http_build_query($parsed));
8✔
300
            }
301
        }
302
    }
303

304
    private function getPostedToken(RequestInterface $request): ?string
305
    {
306
        assert($request instanceof IncomingRequest);
307

308
        // Does the token exist in POST, HEADER or optionally php:://input - json data or PUT, DELETE, PATCH - raw data.
309

310
        if ($tokenValue = $request->getPost($this->config->tokenName)) {
36✔
311
            return $tokenValue;
15✔
312
        }
313

314
        if ($request->hasHeader($this->config->headerName)
21✔
315
            && $request->header($this->config->headerName)->getValue() !== ''
21✔
316
            && $request->header($this->config->headerName)->getValue() !== []) {
21✔
317
            return $request->header($this->config->headerName)->getValue();
10✔
318
        }
319

320
        $body = (string) $request->getBody();
11✔
321

322
        if ($body !== '') {
11✔
323
            $json = json_decode($body);
10✔
324
            if ($json !== null && json_last_error() === JSON_ERROR_NONE) {
10✔
325
                return $json->{$this->config->tokenName} ?? null;
6✔
326
            }
327

328
            parse_str($body, $parsed);
4✔
329

330
            return $parsed[$this->config->tokenName] ?? null;
4✔
331
        }
332

333
        return null;
1✔
334
    }
335

336
    /**
337
     * Returns the CSRF Token.
338
     */
339
    public function getHash(): ?string
340
    {
341
        return $this->config->tokenRandomize ? $this->randomize($this->hash) : $this->hash;
18✔
342
    }
343

344
    /**
345
     * Randomize hash to avoid BREACH attacks.
346
     *
347
     * @params string $hash CSRF hash
348
     *
349
     * @return string CSRF token
350
     */
351
    protected function randomize(string $hash): string
352
    {
353
        $keyBinary  = random_bytes(static::CSRF_HASH_BYTES);
1✔
354
        $hashBinary = hex2bin($hash);
1✔
355

356
        if ($hashBinary === false) {
1✔
UNCOV
357
            throw new LogicException('$hash is invalid: ' . $hash);
×
358
        }
359

360
        return bin2hex(($hashBinary ^ $keyBinary) . $keyBinary);
1✔
361
    }
362

363
    /**
364
     * Derandomize the token.
365
     *
366
     * @params string $token CSRF token
367
     *
368
     * @return string CSRF hash
369
     *
370
     * @throws InvalidArgumentException "hex2bin(): Hexadecimal input string must have an even length"
371
     */
372
    protected function derandomize(string $token): string
373
    {
374
        $key   = substr($token, -static::CSRF_HASH_BYTES * 2);
13✔
375
        $value = substr($token, 0, static::CSRF_HASH_BYTES * 2);
13✔
376

377
        try {
378
            return bin2hex(hex2bin($value) ^ hex2bin($key));
13✔
379
        } catch (ErrorException $e) {
1✔
380
            // "hex2bin(): Hexadecimal input string must have an even length"
381
            throw new InvalidArgumentException($e->getMessage());
1✔
382
        }
383
    }
384

385
    /**
386
     * Returns the CSRF Token Name.
387
     */
388
    public function getTokenName(): string
389
    {
390
        return $this->config->tokenName;
4✔
391
    }
392

393
    /**
394
     * Returns the CSRF Header Name.
395
     */
396
    public function getHeaderName(): string
397
    {
398
        return $this->config->headerName;
3✔
399
    }
400

401
    /**
402
     * Returns the CSRF Cookie Name.
403
     */
404
    public function getCookieName(): string
405
    {
406
        return $this->config->cookieName;
3✔
407
    }
408

409
    /**
410
     * Check if request should be redirect on failure.
411
     */
412
    public function shouldRedirect(): bool
413
    {
414
        return $this->config->redirect;
1✔
415
    }
416

417
    /**
418
     * Sanitize Filename
419
     *
420
     * Tries to sanitize filenames in order to prevent directory traversal attempts
421
     * and other security threats, which is particularly useful for files that
422
     * were supplied via user input.
423
     *
424
     * If it is acceptable for the user input to include relative paths,
425
     * e.g. file/in/some/approved/folder.txt, you can set the second optional
426
     * parameter, $relative_path to TRUE.
427
     *
428
     * @param string $str          Input file name
429
     * @param bool   $relativePath Whether to preserve paths
430
     */
431
    public function sanitizeFilename(string $str, bool $relativePath = false): string
432
    {
433
        // List of sanitize filename strings
434
        $bad = [
3✔
435
            '../',
3✔
436
            '<!--',
3✔
437
            '-->',
3✔
438
            '<',
3✔
439
            '>',
3✔
440
            "'",
3✔
441
            '"',
3✔
442
            '&',
3✔
443
            '$',
3✔
444
            '#',
3✔
445
            '{',
3✔
446
            '}',
3✔
447
            '[',
3✔
448
            ']',
3✔
449
            '=',
3✔
450
            ';',
3✔
451
            '?',
3✔
452
            '%20',
3✔
453
            '%22',
3✔
454
            '%3c',
3✔
455
            '%253c',
3✔
456
            '%3e',
3✔
457
            '%0e',
3✔
458
            '%28',
3✔
459
            '%29',
3✔
460
            '%2528',
3✔
461
            '%26',
3✔
462
            '%24',
3✔
463
            '%3f',
3✔
464
            '%3b',
3✔
465
            '%3d',
3✔
466
        ];
3✔
467

468
        if (! $relativePath) {
3✔
469
            $bad[] = './';
3✔
470
            $bad[] = '/';
3✔
471
        }
472

473
        $str = remove_invisible_characters($str, false);
3✔
474

475
        do {
476
            $old = $str;
3✔
477
            $str = str_replace($bad, '', $str);
3✔
478
        } while ($old !== $str);
3✔
479

480
        return stripslashes($str);
3✔
481
    }
482

483
    /**
484
     * Restore hash from Session or Cookie
485
     */
486
    private function restoreHash(): void
487
    {
488
        if ($this->isCSRFCookie()) {
94✔
489
            if ($this->isHashInCookie()) {
68✔
490
                $this->hash = $this->hashInCookie;
68✔
491
            }
492
        } elseif ($this->session->has($this->config->tokenName)) {
26✔
493
            // Session based CSRF protection
494
            $this->hash = $this->session->get($this->config->tokenName);
26✔
495
        }
496
    }
497

498
    /**
499
     * Generates (Regenerates) the CSRF Hash.
500
     */
501
    public function generateHash(): string
502
    {
503
        $this->hash = bin2hex(random_bytes(static::CSRF_HASH_BYTES));
73✔
504

505
        if ($this->isCSRFCookie()) {
73✔
506
            $this->saveHashInCookie();
61✔
507
        } else {
508
            // Session based CSRF protection
509
            $this->saveHashInSession();
12✔
510
        }
511

512
        return $this->hash;
73✔
513
    }
514

515
    private function isHashInCookie(): bool
516
    {
517
        if ($this->hashInCookie === null) {
68✔
518
            return false;
54✔
519
        }
520

521
        $length  = static::CSRF_HASH_BYTES * 2;
15✔
522
        $pattern = '#^[0-9a-f]{' . $length . '}$#iS';
15✔
523

524
        return preg_match($pattern, $this->hashInCookie) === 1;
15✔
525
    }
526

527
    private function saveHashInCookie(): void
528
    {
529
        $this->cookie = new Cookie(
61✔
530
            $this->rawCookieName,
61✔
531
            $this->hash,
61✔
532
            [
61✔
533
                'expires' => $this->config->expires === 0 ? 0 : Time::now()->getTimestamp() + $this->config->expires,
61✔
534
            ]
61✔
535
        );
61✔
536

537
        $response = Services::response();
61✔
538
        $response->setCookie($this->cookie);
61✔
539
    }
540

541
    private function saveHashInSession(): void
542
    {
543
        $this->session->set($this->config->tokenName, $this->hash);
12✔
544
    }
545
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc