Skip to content

Commit 97a6d66

Browse files
datlechinmichalsnpaulbalandan
authored
fix: ensure csrf token is string (#9365)
* fix: ensure csrf token is string * fix: ensure csrf token is string * handle request php://input * handle request php://input * wip * wip * wip * wip Co-authored-by: Michal Sniatala <[email protected]> * wip Co-authored-by: Michal Sniatala <[email protected]> * Update user_guide_src/source/changelogs/v4.5.8.rst * Use data providers --------- Co-authored-by: Michal Sniatala <[email protected]> Co-authored-by: John Paul E. Balandan, CPA <[email protected]>
1 parent 5f8aa24 commit 97a6d66

File tree

3 files changed

+94
-22
lines changed

3 files changed

+94
-22
lines changed

system/Security/Security.php

+10-7
Original file line numberDiff line numberDiff line change
@@ -307,26 +307,29 @@ private function getPostedToken(RequestInterface $request): ?string
307307
// Does the token exist in POST, HEADER or optionally php:://input - json data or PUT, DELETE, PATCH - raw data.
308308

309309
if ($tokenValue = $request->getPost($this->config->tokenName)) {
310-
return $tokenValue;
310+
return is_string($tokenValue) ? $tokenValue : null;
311311
}
312312

313-
if ($request->hasHeader($this->config->headerName)
314-
&& $request->header($this->config->headerName)->getValue() !== ''
315-
&& $request->header($this->config->headerName)->getValue() !== []) {
316-
return $request->header($this->config->headerName)->getValue();
313+
if ($request->hasHeader($this->config->headerName)) {
314+
$tokenValue = $request->header($this->config->headerName)->getValue();
315+
316+
return (is_string($tokenValue) && $tokenValue !== '') ? $tokenValue : null;
317317
}
318318

319319
$body = (string) $request->getBody();
320320

321321
if ($body !== '') {
322322
$json = json_decode($body);
323323
if ($json !== null && json_last_error() === JSON_ERROR_NONE) {
324-
return $json->{$this->config->tokenName} ?? null;
324+
$tokenValue = $json->{$this->config->tokenName} ?? null;
325+
326+
return is_string($tokenValue) ? $tokenValue : null;
325327
}
326328

327329
parse_str($body, $parsed);
330+
$tokenValue = $parsed[$this->config->tokenName] ?? null;
328331

329-
return $parsed[$this->config->tokenName] ?? null;
332+
return is_string($tokenValue) ? $tokenValue : null;
330333
}
331334

332335
return null;

tests/system/Security/SecurityTest.php

+83-15
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use CodeIgniter\Test\Mock\MockSecurity;
2525
use Config\Security as SecurityConfig;
2626
use PHPUnit\Framework\Attributes\BackupGlobals;
27+
use PHPUnit\Framework\Attributes\DataProvider;
2728
use PHPUnit\Framework\Attributes\Group;
2829

2930
/**
@@ -42,13 +43,23 @@ protected function setUp(): void
4243
$this->resetServices();
4344
}
4445

45-
private function createMockSecurity(?SecurityConfig $config = null): MockSecurity
46+
private static function createMockSecurity(SecurityConfig $config = new SecurityConfig()): MockSecurity
4647
{
47-
$config ??= new SecurityConfig();
48-
4948
return new MockSecurity($config);
5049
}
5150

51+
private static function createIncomingRequest(): IncomingRequest
52+
{
53+
$config = new MockAppConfig();
54+
55+
return new IncomingRequest(
56+
$config,
57+
new SiteURI($config),
58+
null,
59+
new UserAgent(),
60+
);
61+
}
62+
5263
public function testBasicConfigIsSaved(): void
5364
{
5465
$security = $this->createMockSecurity();
@@ -108,18 +119,6 @@ public function testCSRFVerifyPostThrowsExceptionOnNoMatch(): void
108119
$security->verify($request);
109120
}
110121

111-
private function createIncomingRequest(): IncomingRequest
112-
{
113-
$config = new MockAppConfig();
114-
115-
return new IncomingRequest(
116-
$config,
117-
new SiteURI($config),
118-
null,
119-
new UserAgent(),
120-
);
121-
}
122-
123122
public function testCSRFVerifyPostReturnsSelfOnMatch(): void
124123
{
125124
$_SERVER['REQUEST_METHOD'] = 'POST';
@@ -315,4 +314,73 @@ public function testGetters(): void
315314
$this->assertIsString($security->getCookieName());
316315
$this->assertIsBool($security->shouldRedirect());
317316
}
317+
318+
public function testGetPostedTokenReturnsTokenFromPost(): void
319+
{
320+
$_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a';
321+
$request = $this->createIncomingRequest();
322+
$method = $this->getPrivateMethodInvoker($this->createMockSecurity(), 'getPostedToken');
323+
324+
$this->assertSame('8b9218a55906f9dcc1dc263dce7f005a', $method($request));
325+
}
326+
327+
public function testGetPostedTokenReturnsTokenFromHeader(): void
328+
{
329+
$_POST = [];
330+
$request = $this->createIncomingRequest()->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005a');
331+
$method = $this->getPrivateMethodInvoker($this->createMockSecurity(), 'getPostedToken');
332+
333+
$this->assertSame('8b9218a55906f9dcc1dc263dce7f005a', $method($request));
334+
}
335+
336+
public function testGetPostedTokenReturnsTokenFromJsonBody(): void
337+
{
338+
$_POST = [];
339+
$jsonBody = json_encode(['csrf_test_name' => '8b9218a55906f9dcc1dc263dce7f005a']);
340+
$request = $this->createIncomingRequest()->setBody($jsonBody);
341+
$method = $this->getPrivateMethodInvoker($this->createMockSecurity(), 'getPostedToken');
342+
343+
$this->assertSame('8b9218a55906f9dcc1dc263dce7f005a', $method($request));
344+
}
345+
346+
public function testGetPostedTokenReturnsTokenFromFormBody(): void
347+
{
348+
$_POST = [];
349+
$formBody = 'csrf_test_name=8b9218a55906f9dcc1dc263dce7f005a';
350+
$request = $this->createIncomingRequest()->setBody($formBody);
351+
$method = $this->getPrivateMethodInvoker($this->createMockSecurity(), 'getPostedToken');
352+
353+
$this->assertSame('8b9218a55906f9dcc1dc263dce7f005a', $method($request));
354+
}
355+
356+
#[DataProvider('provideGetPostedTokenReturnsNullForInvalidInputs')]
357+
public function testGetPostedTokenReturnsNullForInvalidInputs(string $case, IncomingRequest $request): void
358+
{
359+
$method = $this->getPrivateMethodInvoker($this->createMockSecurity(), 'getPostedToken');
360+
361+
$this->assertNull(
362+
$method($request),
363+
sprintf('Failed asserting that %s returns null on invalid input.', $case),
364+
);
365+
}
366+
367+
/**
368+
* @return iterable<string, array{string, IncomingRequest}>
369+
*/
370+
public static function provideGetPostedTokenReturnsNullForInvalidInputs(): iterable
371+
{
372+
$testCases = [
373+
'empty_post' => self::createIncomingRequest(),
374+
'invalid_post_data' => self::createIncomingRequest()->setGlobal('post', ['csrf_test_name' => ['invalid' => 'data']]),
375+
'empty_header' => self::createIncomingRequest()->setHeader('X-CSRF-TOKEN', ''),
376+
'invalid_json_data' => self::createIncomingRequest()->setBody(json_encode(['csrf_test_name' => ['invalid' => 'data']])),
377+
'invalid_json' => self::createIncomingRequest()->setBody('{invalid json}'),
378+
'missing_token_in_body' => self::createIncomingRequest()->setBody('other=value&another=test'),
379+
'invalid_form_data' => self::createIncomingRequest()->setBody('csrf_test_name[]=invalid'),
380+
];
381+
382+
foreach ($testCases as $case => $request) {
383+
yield $case => [$case, $request];
384+
}
385+
}
318386
}

user_guide_src/source/changelogs/v4.5.8.rst

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Bugs Fixed
3939
**********
4040

4141
- **Database:** Fixed a bug where ``Builder::affectedRows()`` threw an error when the previous query call failed in ``Postgre`` and ``SQLSRV`` drivers.
42+
- **Security:** Fixed a bug where the CSRF token validation could fail on malformed input, causing a generic HTTP 500 status code instead of handling the input gracefully.
4243

4344
See the repo's
4445
`CHANGELOG.md <https://github.com/codeigniter4/CodeIgniter4/blob/develop/CHANGELOG.md>`_

0 commit comments

Comments
 (0)