Skip to content

Commit 0a032d1

Browse files
committed
Merge branch '4.3' into feature-timer-record-callable
2 parents b5d5591 + e7a2712 commit 0a032d1

File tree

11 files changed

+213
-78
lines changed

11 files changed

+213
-78
lines changed

app/Views/errors/html/error_404.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta charset="utf-8">
5-
<title>404 Page Not Found</title>
5+
<title><?= lang('Errors.pageNotFound') ?></title>
66

77
<style>
88
div.logo {
@@ -70,13 +70,13 @@
7070
</head>
7171
<body>
7272
<div class="wrap">
73-
<h1>404 - File Not Found</h1>
73+
<h1>404</h1>
7474

7575
<p>
7676
<?php if (! empty($message) && $message !== '(null)') : ?>
7777
<?= nl2br(esc($message)) ?>
7878
<?php else : ?>
79-
Sorry! Cannot seem to find the page you were looking for.
79+
<?= lang('Errors.sorryCannotFind') ?>
8080
<?php endif ?>
8181
</p>
8282
</div>

system/Database/OCI8/Result.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
use CodeIgniter\Database\BaseResult;
1515
use CodeIgniter\Database\ResultInterface;
16-
use CodeIgniter\Entity;
16+
use CodeIgniter\Entity\Entity;
1717

1818
/**
1919
* Result for OCI8

system/Language/en/Errors.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
// Errors language settings
1313
return [
14-
'whoops' => 'Whoops!',
15-
'weHitASnag' => 'We seem to have hit a snag. Please try again later...',
14+
'pageNotFound' => '404 - Page Not Found',
15+
'sorryCannotFind' => 'Sorry! Cannot seem to find the page you were looking for.',
16+
'whoops' => 'Whoops!',
17+
'weHitASnag' => 'We seem to have hit a snag. Please try again later...',
1618
];

system/Security/Security.php

Lines changed: 71 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class Security implements SecurityInterface
5454
protected $tokenRandomize = false;
5555

5656
/**
57-
* CSRF Hash
57+
* CSRF Hash (without randomization)
5858
*
5959
* Random hash for Cross Site Request Forgery protection.
6060
*
@@ -88,7 +88,7 @@ class Security implements SecurityInterface
8888
protected $cookie;
8989

9090
/**
91-
* CSRF Cookie Name
91+
* CSRF Cookie Name (with Prefix)
9292
*
9393
* Cookie name for Cross Site Request Forgery protection.
9494
*
@@ -154,6 +154,14 @@ class Security implements SecurityInterface
154154
*/
155155
private ?Session $session = null;
156156

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+
157165
/**
158166
* Constructor.
159167
*
@@ -192,9 +200,13 @@ public function __construct(App $config)
192200
$this->configureSession();
193201
}
194202

195-
$this->request = Services::request();
203+
$this->request = Services::request();
204+
$this->hashInCookie = $this->request->getCookie($this->cookieName);
196205

197-
$this->generateHash();
206+
$this->restoreHash();
207+
if ($this->hash === null) {
208+
$this->generateHash();
209+
}
198210
}
199211

200212
private function isCSRFCookie(): bool
@@ -240,7 +252,7 @@ public function CSRFVerify(RequestInterface $request)
240252
}
241253

242254
/**
243-
* Returns the CSRF Hash.
255+
* Returns the CSRF Token.
244256
*
245257
* @deprecated Use `CodeIgniter\Security\Security::getHash()` instead of using this method.
246258
*
@@ -293,6 +305,22 @@ public function verify(RequestInterface $request)
293305
throw SecurityException::forDisallowedAction();
294306
}
295307

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+
{
296324
$json = json_decode($request->getBody() ?? '');
297325

298326
if (isset($_POST[$this->tokenName])) {
@@ -304,22 +332,6 @@ public function verify(RequestInterface $request)
304332
unset($json->{$this->tokenName});
305333
$request->setBody(json_encode($json));
306334
}
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;
323335
}
324336

325337
private function getPostedToken(RequestInterface $request): ?string
@@ -342,7 +354,7 @@ private function getPostedToken(RequestInterface $request): ?string
342354
}
343355

344356
/**
345-
* Returns the CSRF Hash.
357+
* Returns the CSRF Token.
346358
*/
347359
public function getHash(): ?string
348360
{
@@ -351,6 +363,10 @@ public function getHash(): ?string
351363

352364
/**
353365
* Randomize hash to avoid BREACH attacks.
366+
*
367+
* @params string $hash CSRF hash
368+
*
369+
* @return string CSRF token
354370
*/
355371
protected function randomize(string $hash): string
356372
{
@@ -367,7 +383,11 @@ protected function randomize(string $hash): string
367383
/**
368384
* Derandomize the token.
369385
*
386+
* @params string $token CSRF token
387+
*
370388
* @throws InvalidArgumentException "hex2bin(): Hexadecimal input string must have an even length"
389+
*
390+
* @return string CSRF hash
371391
*/
372392
protected function derandomize(string $token): string
373393
{
@@ -493,42 +513,47 @@ public function sanitizeFilename(string $str, bool $relativePath = false): strin
493513
}
494514

495515
/**
496-
* Generates the CSRF Hash.
516+
* Restore hash from Session or Cookie
497517
*/
498-
protected function generateHash(): string
518+
private function restoreHash(): void
499519
{
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;
512523
}
524+
} elseif ($this->session->has($this->tokenName)) {
525+
// Session based CSRF protection
526+
$this->hash = $this->session->get($this->tokenName);
527+
}
528+
}
513529

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));
515536

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();
522542
}
523543

524544
return $this->hash;
525545
}
526546

527547
private function isHashInCookie(): bool
528548
{
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;
532557
}
533558

534559
private function saveHashInCookie(): void
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
/**
4+
* This file is part of CodeIgniter 4 framework.
5+
*
6+
* (c) CodeIgniter Foundation <[email protected]>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace CodeIgniter\Security;
13+
14+
use CodeIgniter\Config\Factories;
15+
use CodeIgniter\Cookie\Cookie;
16+
use CodeIgniter\HTTP\IncomingRequest;
17+
use CodeIgniter\HTTP\URI;
18+
use CodeIgniter\HTTP\UserAgent;
19+
use CodeIgniter\Test\CIUnitTestCase;
20+
use CodeIgniter\Test\Mock\MockAppConfig;
21+
use CodeIgniter\Test\Mock\MockSecurity;
22+
use Config\Security as SecurityConfig;
23+
24+
/**
25+
* @internal
26+
*/
27+
final class SecurityCSRFCookieRandomizeTokenTest extends CIUnitTestCase
28+
{
29+
/**
30+
* @var string CSRF protection hash
31+
*/
32+
private string $hash = '8b9218a55906f9dcc1dc263dce7f005a';
33+
34+
/**
35+
* @var string CSRF randomized token
36+
*/
37+
private string $randomizedToken = '8bc70b67c91494e815c7d2219c1ae0ab005513c290126d34d41bf41c5265e0f1';
38+
39+
protected function setUp(): void
40+
{
41+
parent::setUp();
42+
43+
$_COOKIE = [];
44+
45+
$config = new SecurityConfig();
46+
$config->csrfProtection = Security::CSRF_PROTECTION_COOKIE;
47+
$config->tokenRandomize = true;
48+
Factories::injectMock('config', 'Security', $config);
49+
50+
// Set Cookie value
51+
$security = new MockSecurity(new MockAppConfig());
52+
$_COOKIE[$security->getCookieName()] = $this->hash;
53+
54+
$this->resetServices();
55+
}
56+
57+
public function testTokenIsReadFromCookie()
58+
{
59+
$security = new MockSecurity(new MockAppConfig());
60+
61+
$this->assertSame(
62+
$this->randomizedToken,
63+
$security->getHash()
64+
);
65+
}
66+
67+
public function testCSRFVerifySetNewCookie()
68+
{
69+
$_SERVER['REQUEST_METHOD'] = 'POST';
70+
$_POST['foo'] = 'bar';
71+
$_POST['csrf_test_name'] = $this->randomizedToken;
72+
73+
$request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent());
74+
75+
$security = new Security(new MockAppConfig());
76+
77+
$this->assertInstanceOf(Security::class, $security->verify($request));
78+
$this->assertLogged('info', 'CSRF token verified.');
79+
$this->assertCount(1, $_POST);
80+
81+
/** @var Cookie $cookie */
82+
$cookie = $this->getPrivateProperty($security, 'cookie');
83+
$newHash = $cookie->getValue();
84+
85+
$this->assertNotSame($this->hash, $newHash);
86+
$this->assertSame(32, strlen($newHash));
87+
}
88+
}

tests/system/Security/SecurityTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ protected function setUp(): void
3636

3737
$_COOKIE = [];
3838

39-
Factories::reset();
39+
$this->resetServices();
4040
}
4141

4242
public function testBasicConfigIsSaved()

user_guide_src/source/changelogs/v4.0.4.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Enhancements:
1616
- New Testing Feature: :doc:`Fabricator </testing/fabricator>` makes creating mock classes simple and repeatable in your tests.
1717
- Model class can now have the callbacks overridden at runtime. Useful for testing.
1818
- A number of improvements to :doc:`Feature Tests </testing/feature>` in general.
19-
- New :doc:`command() helper function </cli/cli_commands>` to programatically run your CLI commands. Useful for testing and cron jobs.
19+
- New :doc:`command() helper function </cli/spark_commands>` to programatically run your CLI commands. Useful for testing and cron jobs.
2020
- New command, ``make:seeder`` to generate a :doc:`Database Seed class </dbmgmt/seeds>` skeleton file.
2121
- Colors now available on the CLI within Windows, as well as other Windows-related CLI improvements.
2222
- New helper :doc:`mb_url_title </helpers/url_helper>` that functions like ``url_title`` but automatically escapes and extended URL characters.

user_guide_src/source/installation/index.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
Installation
33
############
44

5-
CodeIgniter4 can be installed in a number of different ways: manually,
6-
using `Composer <https://getcomposer.org>`_, or using
7-
`Git <https://git-scm.com/>`_.
5+
CodeIgniter has two supported installation methods: manual download,
6+
or using `Composer <https://getcomposer.org>`_.
87
Which is right for you?
98

109
- We recommend the Composer installation because it keeps CodeIgniter up to date easily.

0 commit comments

Comments
 (0)