Skip to content

Commit aba1ac1

Browse files
authored
Merge pull request #6282 from kenjis/fix-cachePage-secureheaders
fix: page cache saves Response data before running after filters
2 parents e92a5d0 + e0964ed commit aba1ac1

File tree

7 files changed

+112
-18
lines changed

7 files changed

+112
-18
lines changed

system/CodeIgniter.php

+30-18
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
use CodeIgniter\HTTP\IncomingRequest;
2222
use CodeIgniter\HTTP\RedirectResponse;
2323
use CodeIgniter\HTTP\Request;
24-
use CodeIgniter\HTTP\RequestInterface;
2524
use CodeIgniter\HTTP\ResponseInterface;
2625
use CodeIgniter\HTTP\URI;
2726
use CodeIgniter\Router\Exceptions\RedirectException;
@@ -55,7 +54,7 @@ class CodeIgniter
5554
/**
5655
* App startup time.
5756
*
58-
* @var mixed
57+
* @var float|null
5958
*/
6059
protected $startTime;
6160

@@ -298,17 +297,18 @@ protected function initializeKint()
298297
* tries to route the response, loads the controller and generally
299298
* makes all of the pieces work together.
300299
*
301-
* @throws Exception
302300
* @throws RedirectException
303301
*
304-
* @return bool|mixed|RequestInterface|ResponseInterface|void
302+
* @return ResponseInterface|void
305303
*/
306304
public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false)
307305
{
308306
if ($this->context === null) {
309307
throw new LogicException('Context must be set before run() is called. If you are upgrading from 4.1.x, you need to merge `public/index.php` and `spark` file from `vendor/codeigniter4/framework`.');
310308
}
311309

310+
static::$cacheTTL = 0;
311+
312312
$this->startBenchmark();
313313

314314
$this->getRequestObject();
@@ -321,7 +321,9 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon
321321
if ($this->request instanceof IncomingRequest && strtolower($this->request->getMethod()) === 'cli') {
322322
$this->response->setStatusCode(405)->setBody('Method Not Allowed');
323323

324-
return $this->sendResponse();
324+
$this->sendResponse();
325+
326+
return;
325327
}
326328

327329
Events::trigger('pre_system');
@@ -409,7 +411,7 @@ private function isWeb(): bool
409411
* @throws PageNotFoundException
410412
* @throws RedirectException
411413
*
412-
* @return mixed|RequestInterface|ResponseInterface
414+
* @return ResponseInterface
413415
*/
414416
protected function handleRequest(?RouteCollectionInterface $routes, Cache $cacheConfig, bool $returnResponse = false)
415417
{
@@ -475,6 +477,9 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
475477
// so it can be used with the output.
476478
$this->gatherOutput($cacheConfig, $returned);
477479

480+
// After filter debug toolbar requires 'total_execution'.
481+
$this->totalTime = $this->benchmark->getElapsedTime('total_execution');
482+
478483
// Never run filters when running through Spark cli
479484
if (! $this->isSparked()) {
480485
$filters->setResponse($this->response);
@@ -496,6 +501,17 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
496501
$this->response = $response;
497502
}
498503

504+
// Cache it without the performance metrics replaced
505+
// so that we can have live speed updates along the way.
506+
// Must be run after filters to preserve the Response headers.
507+
if (static::$cacheTTL > 0) {
508+
$this->cachePage($cacheConfig);
509+
}
510+
511+
// Update the performance metrics
512+
$output = $this->displayPerformanceMetrics($this->response->getBody());
513+
$this->response->setBody($output);
514+
499515
// Save our current URI as the previous URI in the session
500516
// for safer, more accurate use with `previous_url()` helper function.
501517
$this->storePreviousURL(current_url(true));
@@ -641,7 +657,7 @@ protected function forceSecureAccess($duration = 31_536_000)
641657
*
642658
* @throws Exception
643659
*
644-
* @return bool|ResponseInterface
660+
* @return false|ResponseInterface
645661
*/
646662
public function displayCache(Cache $config)
647663
{
@@ -664,7 +680,8 @@ public function displayCache(Cache $config)
664680
$this->response->setHeader($name, $value);
665681
}
666682

667-
$output = $this->displayPerformanceMetrics($output);
683+
$this->totalTime = $this->benchmark->getElapsedTime('total_execution');
684+
$output = $this->displayPerformanceMetrics($output);
668685
$this->response->setBody($output);
669686

670687
return $this->response;
@@ -734,8 +751,6 @@ protected function generateCacheName(Cache $config): string
734751
*/
735752
public function displayPerformanceMetrics(string $output): string
736753
{
737-
$this->totalTime = $this->benchmark->getElapsedTime('total_execution');
738-
739754
return str_replace('{elapsed_time}', (string) $this->totalTime, $output);
740755
}
741756

@@ -956,7 +971,10 @@ protected function display404errors(PageNotFoundException $e)
956971
* Gathers the script output from the buffer, replaces some execution
957972
* time tag in the output and displays the debug toolbar, if required.
958973
*
974+
* @param Cache|null $cacheConfig Deprecated. No longer used.
959975
* @param ResponseInterface|string|null $returned
976+
*
977+
* @deprecated $cacheConfig is deprecated.
960978
*/
961979
protected function gatherOutput(?Cache $cacheConfig = null, $returned = null)
962980
{
@@ -992,14 +1010,6 @@ protected function gatherOutput(?Cache $cacheConfig = null, $returned = null)
9921010
$this->output .= $returned;
9931011
}
9941012

995-
// Cache it without the performance metrics replaced
996-
// so that we can have live speed updates along the way.
997-
if (static::$cacheTTL > 0) {
998-
$this->cachePage($cacheConfig);
999-
}
1000-
1001-
$this->output = $this->displayPerformanceMetrics($this->output);
1002-
10031013
$this->response->setBody($this->output);
10041014
}
10051015

@@ -1069,6 +1079,8 @@ public function spoofRequestMethod()
10691079
/**
10701080
* Sends the output of this request back to the client.
10711081
* This is what they've been waiting for!
1082+
*
1083+
* @return void
10721084
*/
10731085
protected function sendResponse()
10741086
{

system/HTTP/DownloadResponse.php

+2
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ public function setCache(array $options = [])
247247
/**
248248
* {@inheritDoc}
249249
*
250+
* @return $this
251+
*
250252
* @todo Do downloads need CSP or Cookies? Compare with ResponseTrait::send()
251253
*/
252254
public function send()

tests/system/CodeIgniterTest.php

+62
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
use CodeIgniter\HTTP\Response;
1616
use CodeIgniter\Router\RouteCollection;
1717
use CodeIgniter\Test\CIUnitTestCase;
18+
use CodeIgniter\Test\Filters\CITestStreamFilter;
1819
use CodeIgniter\Test\Mock\MockCodeIgniter;
1920
use Config\App;
21+
use Config\Filters;
2022
use Config\Modules;
2123
use Tests\Support\Filters\Customfilter;
2224

@@ -531,4 +533,64 @@ public function testSpoofRequestMethodCannotUseGET()
531533

532534
$this->assertSame('post', Services::request()->getMethod());
533535
}
536+
537+
/**
538+
* @see https://github.com/codeigniter4/CodeIgniter4/issues/6281
539+
*/
540+
public function testPageCacheSendSecureHeaders()
541+
{
542+
// Suppress command() output
543+
CITestStreamFilter::$buffer = '';
544+
$outputStreamFilter = stream_filter_append(STDOUT, 'CITestStreamFilter');
545+
$errorStreamFilter = stream_filter_append(STDERR, 'CITestStreamFilter');
546+
547+
// Clear Page cache
548+
command('cache:clear');
549+
550+
$_SERVER['REQUEST_URI'] = '/test';
551+
552+
$routes = Services::routes();
553+
$routes->add('test', static function () {
554+
CodeIgniter::cache(3600);
555+
556+
$response = Services::response();
557+
$string = 'This is a test page. Elapsed time: {elapsed_time}';
558+
559+
return $response->setBody($string);
560+
});
561+
$router = Services::router($routes, Services::request());
562+
Services::injectMock('router', $router);
563+
564+
/** @var Filters $filterConfig */
565+
$filterConfig = config('Filters');
566+
$filterConfig->globals['after'] = ['secureheaders'];
567+
Services::filters($filterConfig);
568+
569+
// The first response to be cached.
570+
ob_start();
571+
$this->codeigniter->useSafeOutput(true)->run();
572+
$output = ob_get_clean();
573+
574+
$this->assertStringContainsString('This is a test page', $output);
575+
$response = Services::response();
576+
$headers = $response->headers();
577+
$this->assertArrayHasKey('X-Frame-Options', $headers);
578+
579+
// The second response from the Page cache.
580+
ob_start();
581+
$this->codeigniter->useSafeOutput(true)->run();
582+
$output = ob_get_clean();
583+
584+
$this->assertStringContainsString('This is a test page', $output);
585+
$response = Services::response();
586+
$headers = $response->headers();
587+
$this->assertArrayHasKey('X-Frame-Options', $headers);
588+
589+
// Clear Page cache
590+
command('cache:clear');
591+
592+
// Remove stream fliters
593+
stream_filter_remove($outputStreamFilter);
594+
stream_filter_remove($errorStreamFilter);
595+
}
534596
}

user_guide_src/source/changelogs/v4.2.2.rst

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ BREAKING
1515
- Now ``Services::request()`` returns ``IncomingRequest`` or ``CLIRequest``.
1616
- The method signature of ``CodeIgniter\Debug\Exceptions::__construct()`` has been changed. The ``IncomingRequest`` typehint on the ``$request`` parameter was removed. Extending classes should likewise remove the parameter so as not to break LSP.
1717
- The method signature of ``BaseBuilder.php::insert()`` and ``BaseBuilder.php::update()`` have been changed. The ``?array`` typehint on the ``$set`` parameter was removed.
18+
- A bug that caused pages to be cached before after filters were executed when using page caching has been fixed. Adding response headers or changing the response body in after filters now caches them correctly.
1819

1920
Enhancements
2021
************
@@ -33,6 +34,7 @@ Deprecations
3334
************
3435

3536
- The parameters of ``Services::request()`` are deprecated.
37+
- The first parameter ``$cacheConfig`` of ``CodeIgniter::gatherOutput()`` is deprecated.
3638

3739
Bugs Fixed
3840
**********

user_guide_src/source/incoming/filters.rst

+4
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ If a ``Response`` instance is returned, the Response will be sent back to the cl
5050
This can be useful for implementing rate limiting for APIs. See :doc:`Throttler </libraries/throttler>` for an
5151
example.
5252

53+
.. _after-filters:
54+
5355
After Filters
5456
=============
5557

@@ -169,6 +171,8 @@ This filter prohibits user input data (``$_GET``, ``$_POST``, ``$_COOKIE``, ``ph
169171
- invalid UTF-8 characters
170172
- control characters except line break and tab code
171173

174+
.. _secureheaders:
175+
172176
SecureHeaders
173177
=============
174178

user_guide_src/source/installation/upgrade_422.rst

+11
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ Mandatory File Changes
1919
Breaking Changes
2020
****************
2121

22+
Web Page Caching Bug Fix
23+
========================
24+
25+
- :doc:`../general/caching` now caches the Response data after :ref:`after-filters` are executed.
26+
- For example, if you enable :ref:`secureheaders`, the Response headers are now sent when the page comes from the cache.
27+
28+
.. important:: If you have written **code based on this bug** that assumes changes to the Response in "after" filters are not cached then **sensitive information could be cached and compromised**. If this is the case, change your code to disable caching of the page.
29+
30+
Others
31+
======
32+
2233
- The method ``Forge::createTable()`` no longer executes a ``CREATE TABLE IF NOT EXISTS``. If table is not found in ``$db->tableExists($table)`` then ``CREATE TABLE`` is executed.
2334
- The second parameter ``$ifNotExists`` of ``Forge::_createTable()`` is deprecated. It is no longer used and will be removed in a future release.
2435

user_guide_src/source/installation/upgrading.rst

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ upgrading from.
1212
.. toctree::
1313
:titlesonly:
1414

15+
upgrade_422
1516
upgrade_421
1617
upgrade_420
1718
upgrade_418

0 commit comments

Comments
 (0)