diff --git a/CHANGELOG.md b/CHANGELOG.md index 23c77082242e..8ecd4ae661e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [v4.1.8](https://github.com/codeigniter4/CodeIgniter4/tree/v4.1.8) (2022-01-24) + +[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.1.7...v4.1.8) + +**SECURITY** + +* *XSS Vulnerability* in the `API\ResponseTrait` was fixed. See the [Security advisory](https://github.com/codeigniter4/CodeIgniter4/security/advisories/GHSA-7528-7jg5-6g62) for more information. + ## [v4.1.7](https://github.com/codeigniter4/CodeIgniter4/tree/v4.1.7) (2022-01-09) [Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.1.6...v4.1.7) diff --git a/admin/framework/composer.json b/admin/framework/composer.json index 6c02a4b71eb9..25ac1f07ecc3 100644 --- a/admin/framework/composer.json +++ b/admin/framework/composer.json @@ -10,7 +10,7 @@ "ext-intl": "*", "ext-json": "*", "ext-mbstring": "*", - "kint-php/kint": "^3.3", + "kint-php/kint": "^4.0", "laminas/laminas-escaper": "^2.9", "psr/log": "^1.1" }, diff --git a/system/API/ResponseTrait.php b/system/API/ResponseTrait.php index 56848ea608de..9ee722b45235 100644 --- a/system/API/ResponseTrait.php +++ b/system/API/ResponseTrait.php @@ -85,9 +85,9 @@ trait ResponseTrait * * @param array|string|null $data * - * @return mixed + * @return Response */ - public function respond($data = null, ?int $status = null, string $message = '') + protected function respond($data = null, ?int $status = null, string $message = '') { if ($data === null && $status === null) { $status = 404; @@ -119,9 +119,9 @@ public function respond($data = null, ?int $status = null, string $message = '') * @param int $status HTTP status code * @param string|null $code Custom, API-specific, error code * - * @return mixed + * @return Response */ - public function fail($messages, int $status = 400, ?string $code = null, string $customMessage = '') + protected function fail($messages, int $status = 400, ?string $code = null, string $customMessage = '') { if (! is_array($messages)) { $messages = ['error' => $messages]; @@ -145,9 +145,9 @@ public function fail($messages, int $status = 400, ?string $code = null, string * * @param mixed $data * - * @return mixed + * @return Response */ - public function respondCreated($data = null, string $message = '') + protected function respondCreated($data = null, string $message = '') { return $this->respond($data, $this->codes['created'], $message); } @@ -157,9 +157,9 @@ public function respondCreated($data = null, string $message = '') * * @param mixed $data * - * @return mixed + * @return Response */ - public function respondDeleted($data = null, string $message = '') + protected function respondDeleted($data = null, string $message = '') { return $this->respond($data, $this->codes['deleted'], $message); } @@ -169,9 +169,9 @@ public function respondDeleted($data = null, string $message = '') * * @param mixed $data * - * @return mixed + * @return Response */ - public function respondUpdated($data = null, string $message = '') + protected function respondUpdated($data = null, string $message = '') { return $this->respond($data, $this->codes['updated'], $message); } @@ -180,9 +180,9 @@ public function respondUpdated($data = null, string $message = '') * Used after a command has been successfully executed but there is no * meaningful reply to send back to the client. * - * @return mixed + * @return Response */ - public function respondNoContent(string $message = 'No Content') + protected function respondNoContent(string $message = 'No Content') { return $this->respond(null, $this->codes['no_content'], $message); } @@ -192,9 +192,9 @@ public function respondNoContent(string $message = 'No Content') * or had bad authorization credentials. User is encouraged to try again * with the proper information. * - * @return mixed + * @return Response */ - public function failUnauthorized(string $description = 'Unauthorized', ?string $code = null, string $message = '') + protected function failUnauthorized(string $description = 'Unauthorized', ?string $code = null, string $message = '') { return $this->fail($description, $this->codes['unauthorized'], $code, $message); } @@ -203,9 +203,9 @@ public function failUnauthorized(string $description = 'Unauthorized', ?string $ * Used when access is always denied to this resource and no amount * of trying again will help. * - * @return mixed + * @return Response */ - public function failForbidden(string $description = 'Forbidden', ?string $code = null, string $message = '') + protected function failForbidden(string $description = 'Forbidden', ?string $code = null, string $message = '') { return $this->fail($description, $this->codes['forbidden'], $code, $message); } @@ -213,9 +213,9 @@ public function failForbidden(string $description = 'Forbidden', ?string $code = /** * Used when a specified resource cannot be found. * - * @return mixed + * @return Response */ - public function failNotFound(string $description = 'Not Found', ?string $code = null, string $message = '') + protected function failNotFound(string $description = 'Not Found', ?string $code = null, string $message = '') { return $this->fail($description, $this->codes['resource_not_found'], $code, $message); } @@ -223,11 +223,11 @@ public function failNotFound(string $description = 'Not Found', ?string $code = /** * Used when the data provided by the client cannot be validated. * - * @return mixed + * @return Response * * @deprecated Use failValidationErrors instead */ - public function failValidationError(string $description = 'Bad Request', ?string $code = null, string $message = '') + protected function failValidationError(string $description = 'Bad Request', ?string $code = null, string $message = '') { return $this->fail($description, $this->codes['invalid_data'], $code, $message); } @@ -237,9 +237,9 @@ public function failValidationError(string $description = 'Bad Request', ?string * * @param string|string[] $errors * - * @return mixed + * @return Response */ - public function failValidationErrors($errors, ?string $code = null, string $message = '') + protected function failValidationErrors($errors, ?string $code = null, string $message = '') { return $this->fail($errors, $this->codes['invalid_data'], $code, $message); } @@ -247,9 +247,9 @@ public function failValidationErrors($errors, ?string $code = null, string $mess /** * Use when trying to create a new resource and it already exists. * - * @return mixed + * @return Response */ - public function failResourceExists(string $description = 'Conflict', ?string $code = null, string $message = '') + protected function failResourceExists(string $description = 'Conflict', ?string $code = null, string $message = '') { return $this->fail($description, $this->codes['resource_exists'], $code, $message); } @@ -259,9 +259,9 @@ public function failResourceExists(string $description = 'Conflict', ?string $co * Not Found, because here we know the data previously existed, but is now gone, * where Not Found means we simply cannot find any information about it. * - * @return mixed + * @return Response */ - public function failResourceGone(string $description = 'Gone', ?string $code = null, string $message = '') + protected function failResourceGone(string $description = 'Gone', ?string $code = null, string $message = '') { return $this->fail($description, $this->codes['resource_gone'], $code, $message); } @@ -269,9 +269,9 @@ public function failResourceGone(string $description = 'Gone', ?string $code = n /** * Used when the user has made too many requests for the resource recently. * - * @return mixed + * @return Response */ - public function failTooManyRequests(string $description = 'Too Many Requests', ?string $code = null, string $message = '') + protected function failTooManyRequests(string $description = 'Too Many Requests', ?string $code = null, string $message = '') { return $this->fail($description, $this->codes['too_many_requests'], $code, $message); } @@ -285,7 +285,7 @@ public function failTooManyRequests(string $description = 'Too Many Requests', ? * * @return Response The value of the Response's send() method. */ - public function failServerError(string $description = 'Internal Server Error', ?string $code = null, string $message = ''): Response + protected function failServerError(string $description = 'Internal Server Error', ?string $code = null, string $message = ''): Response { return $this->fail($description, $this->codes['server_error'], $code, $message); } @@ -346,7 +346,7 @@ protected function format($data = null) * * @return $this */ - public function setResponseFormat(?string $format = null) + protected function setResponseFormat(?string $format = null) { $this->format = strtolower($format); diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index ed4e500466f6..24ad148269cf 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -45,7 +45,7 @@ class CodeIgniter /** * The current version of CodeIgniter Framework */ - public const CI_VERSION = '4.1.7'; + public const CI_VERSION = '4.1.8'; private const MIN_PHP_VERSION = '7.3'; diff --git a/tests/system/API/ResponseTraitTest.php b/tests/system/API/ResponseTraitTest.php index abff4ce0c1a9..8cf06372dcdd 100644 --- a/tests/system/API/ResponseTraitTest.php +++ b/tests/system/API/ResponseTraitTest.php @@ -110,7 +110,8 @@ public function testNoFormatterJSON() { $this->formatter = null; $controller = $this->makeController([], 'http://codeigniter.com', ['Accept' => 'application/json']); - $controller->respondCreated(['id' => 3], 'A Custom Reason'); + + $this->invoke($controller, 'respondCreated', [['id' => 3], 'A Custom Reason']); $this->assertSame('A Custom Reason', $this->response->getReason()); $this->assertSame(201, $this->response->getStatusCode()); @@ -127,7 +128,8 @@ public function testNoFormatter() { $this->formatter = null; $controller = $this->makeController([], 'http://codeigniter.com', ['Accept' => 'application/json']); - $controller->respondCreated('A Custom Reason'); + + $this->invoke($controller, 'respondCreated', ['A Custom Reason']); $this->assertSame('A Custom Reason', $this->response->getBody()); } @@ -137,12 +139,14 @@ public function testAssociativeArrayPayload() $this->formatter = null; $controller = $this->makeController(); $payload = ['answer' => 42]; - $expected = <<<'EOH' + + $this->invoke($controller, 'respond', [$payload]); + + $expected = <<<'EOH' { "answer": 42 } EOH; - $controller->respond($payload); $this->assertSame($expected, $this->response->getBody()); } @@ -155,6 +159,9 @@ public function testArrayPayload() 2, 3, ]; + + $this->invoke($controller, 'respond', [$payload]); + $expected = <<<'EOH' [ 1, @@ -162,7 +169,6 @@ public function testArrayPayload() 3 ] EOH; - $controller->respond($payload); $this->assertSame($expected, $this->response->getBody()); } @@ -173,20 +179,23 @@ public function testPHPtoArrayPayload() $payload = new stdClass(); $payload->name = 'Tom'; $payload->id = 1; - $expected = <<<'EOH' + + $this->invoke($controller, 'respond', [(array) $payload]); + + $expected = <<<'EOH' { "name": "Tom", "id": 1 } EOH; - $controller->respond((array) $payload); $this->assertSame($expected, $this->response->getBody()); } public function testRespondSets404WithNoData() { $controller = $this->makeController(); - $controller->respond(null, null); + + $this->invoke($controller, 'respond', [null, null]); $this->assertSame(404, $this->response->getStatusCode()); $this->assertNull($this->response->getBody()); @@ -195,7 +204,8 @@ public function testRespondSets404WithNoData() public function testRespondSetsStatusWithEmptyData() { $controller = $this->makeController(); - $controller->respond(null, 201); + + $this->invoke($controller, 'respond', [null, 201]); $this->assertSame(201, $this->response->getStatusCode()); $this->assertNull($this->response->getBody()); @@ -204,7 +214,8 @@ public function testRespondSetsStatusWithEmptyData() public function testRespondSetsCorrectBodyAndStatus() { $controller = $this->makeController(); - $controller->respond('something', 201); + + $this->invoke($controller, 'respond', ['something', 201]); $this->assertSame(201, $this->response->getStatusCode()); $this->assertSame('something', $this->response->getBody()); @@ -215,7 +226,8 @@ public function testRespondSetsCorrectBodyAndStatus() public function testRespondWithCustomReason() { $controller = $this->makeController(); - $controller->respond('something', 201, 'A Custom Reason'); + + $this->invoke($controller, 'respond', ['something', 201, 'A Custom Reason']); $this->assertSame(201, $this->response->getStatusCode()); $this->assertSame('A Custom Reason', $this->response->getReason()); @@ -225,7 +237,7 @@ public function testFailSingleMessage() { $controller = $this->makeController(); - $controller->fail('Failure to Launch', 500, 'WHAT!', 'A Custom Reason'); + $this->invoke($controller, 'fail', ['Failure to Launch', 500, 'WHAT!', 'A Custom Reason']); // Will use the JSON formatter by default $expected = [ @@ -235,7 +247,6 @@ public function testFailSingleMessage() 'error' => 'Failure to Launch', ], ]; - $this->assertSame($this->formatter->format($expected), $this->response->getBody()); $this->assertSame(500, $this->response->getStatusCode()); $this->assertSame('A Custom Reason', $this->response->getReason()); @@ -244,7 +255,8 @@ public function testFailSingleMessage() public function testCreated() { $controller = $this->makeController(); - $controller->respondCreated(['id' => 3], 'A Custom Reason'); + + $this->invoke($controller, 'respondCreated', [['id' => 3], 'A Custom Reason']); $this->assertSame('A Custom Reason', $this->response->getReason()); $this->assertSame(201, $this->response->getStatusCode()); @@ -254,7 +266,8 @@ public function testCreated() public function testDeleted() { $controller = $this->makeController(); - $controller->respondDeleted(['id' => 3], 'A Custom Reason'); + + $this->invoke($controller, 'respondDeleted', [['id' => 3], 'A Custom Reason']); $this->assertSame('A Custom Reason', $this->response->getReason()); $this->assertSame(200, $this->response->getStatusCode()); @@ -264,7 +277,8 @@ public function testDeleted() public function testUpdated() { $controller = $this->makeController(); - $controller->respondUpdated(['id' => 3], 'A Custom Reason'); + + $this->invoke($controller, 'respondUpdated', [['id' => 3], 'A Custom Reason']); $this->assertSame('A Custom Reason', $this->response->getReason()); $this->assertSame(200, $this->response->getStatusCode()); @@ -274,7 +288,8 @@ public function testUpdated() public function testUnauthorized() { $controller = $this->makeController(); - $controller->failUnauthorized('Nope', 'FAT CHANCE', 'A Custom Reason'); + + $this->invoke($controller, 'failUnauthorized', ['Nope', 'FAT CHANCE', 'A Custom Reason']); $expected = [ 'status' => 401, @@ -283,7 +298,6 @@ public function testUnauthorized() 'error' => 'Nope', ], ]; - $this->assertSame('A Custom Reason', $this->response->getReason()); $this->assertSame(401, $this->response->getStatusCode()); $this->assertSame($this->formatter->format($expected), $this->response->getBody()); @@ -292,7 +306,8 @@ public function testUnauthorized() public function testForbidden() { $controller = $this->makeController(); - $controller->failForbidden('Nope', 'FAT CHANCE', 'A Custom Reason'); + + $this->invoke($controller, 'failForbidden', ['Nope', 'FAT CHANCE', 'A Custom Reason']); $expected = [ 'status' => 403, @@ -301,7 +316,6 @@ public function testForbidden() 'error' => 'Nope', ], ]; - $this->assertSame('A Custom Reason', $this->response->getReason()); $this->assertSame(403, $this->response->getStatusCode()); $this->assertSame($this->formatter->format($expected), $this->response->getBody()); @@ -310,7 +324,8 @@ public function testForbidden() public function testNoContent() { $controller = $this->makeController(); - $controller->respondNoContent(''); + + $this->invoke($controller, 'respondNoContent', ['']); $this->assertSame('No Content', $this->response->getReason()); $this->assertSame(204, $this->response->getStatusCode()); @@ -319,7 +334,8 @@ public function testNoContent() public function testNotFound() { $controller = $this->makeController(); - $controller->failNotFound('Nope', 'FAT CHANCE', 'A Custom Reason'); + + $this->invoke($controller, 'failNotFound', ['Nope', 'FAT CHANCE', 'A Custom Reason']); $expected = [ 'status' => 404, @@ -328,7 +344,6 @@ public function testNotFound() 'error' => 'Nope', ], ]; - $this->assertSame('A Custom Reason', $this->response->getReason()); $this->assertSame(404, $this->response->getStatusCode()); $this->assertSame($this->formatter->format($expected), $this->response->getBody()); @@ -337,7 +352,8 @@ public function testNotFound() public function testValidationError() { $controller = $this->makeController(); - $controller->failValidationError('Nope', 'FAT CHANCE', 'A Custom Reason'); + + $this->invoke($controller, 'failValidationError', ['Nope', 'FAT CHANCE', 'A Custom Reason']); $expected = [ 'status' => 400, @@ -346,7 +362,6 @@ public function testValidationError() 'error' => 'Nope', ], ]; - $this->assertSame('A Custom Reason', $this->response->getReason()); $this->assertSame(400, $this->response->getStatusCode()); $this->assertSame($this->formatter->format($expected), $this->response->getBody()); @@ -355,7 +370,8 @@ public function testValidationError() public function testValidationErrors() { $controller = $this->makeController(); - $controller->failValidationErrors(['foo' => 'Nope', 'bar' => 'No way'], 'FAT CHANCE', 'A Custom Reason'); + + $this->invoke($controller, 'failValidationErrors', [['foo' => 'Nope', 'bar' => 'No way'], 'FAT CHANCE', 'A Custom Reason']); $expected = [ 'status' => 400, @@ -365,7 +381,6 @@ public function testValidationErrors() 'bar' => 'No way', ], ]; - $this->assertSame('A Custom Reason', $this->response->getReason()); $this->assertSame(400, $this->response->getStatusCode()); $this->assertSame($this->formatter->format($expected), $this->response->getBody()); @@ -374,7 +389,8 @@ public function testValidationErrors() public function testResourceExists() { $controller = $this->makeController(); - $controller->failResourceExists('Nope', 'FAT CHANCE', 'A Custom Reason'); + + $this->invoke($controller, 'failResourceExists', ['Nope', 'FAT CHANCE', 'A Custom Reason']); $expected = [ 'status' => 409, @@ -383,7 +399,6 @@ public function testResourceExists() 'error' => 'Nope', ], ]; - $this->assertSame('A Custom Reason', $this->response->getReason()); $this->assertSame(409, $this->response->getStatusCode()); $this->assertSame($this->formatter->format($expected), $this->response->getBody()); @@ -392,7 +407,8 @@ public function testResourceExists() public function testResourceGone() { $controller = $this->makeController(); - $controller->failResourceGone('Nope', 'FAT CHANCE', 'A Custom Reason'); + + $this->invoke($controller, 'failResourceGone', ['Nope', 'FAT CHANCE', 'A Custom Reason']); $expected = [ 'status' => 410, @@ -401,7 +417,6 @@ public function testResourceGone() 'error' => 'Nope', ], ]; - $this->assertSame('A Custom Reason', $this->response->getReason()); $this->assertSame(410, $this->response->getStatusCode()); $this->assertSame($this->formatter->format($expected), $this->response->getBody()); @@ -410,7 +425,8 @@ public function testResourceGone() public function testTooManyRequests() { $controller = $this->makeController(); - $controller->failTooManyRequests('Nope', 'FAT CHANCE', 'A Custom Reason'); + + $this->invoke($controller, 'failTooManyRequests', ['Nope', 'FAT CHANCE', 'A Custom Reason']); $expected = [ 'status' => 429, @@ -419,7 +435,6 @@ public function testTooManyRequests() 'error' => 'Nope', ], ]; - $this->assertSame('A Custom Reason', $this->response->getReason()); $this->assertSame(429, $this->response->getStatusCode()); $this->assertSame($this->formatter->format($expected), $this->response->getBody()); @@ -428,7 +443,8 @@ public function testTooManyRequests() public function testServerError() { $controller = $this->makeController(); - $controller->failServerError('Nope.', 'FAT-CHANCE', 'A custom reason.'); + + $this->invoke($controller, 'failServerError', ['Nope.', 'FAT-CHANCE', 'A custom reason.']); $this->assertSame('A custom reason.', $this->response->getReason()); $this->assertSame(500, $this->response->getStatusCode()); @@ -491,7 +507,8 @@ public function testXMLFormatter() $this->assertInstanceOf('CodeIgniter\Format\XMLFormatter', $this->formatter); - $controller->respondCreated(['id' => 3], 'A Custom Reason'); + $this->invoke($controller, 'respondCreated', [['id' => 3], 'A Custom Reason']); + $expected = <<<'EOH' 3 @@ -540,23 +557,24 @@ public function __construct(&$request, &$response) } }; - $controller->respondCreated(['id' => 3], 'A Custom Reason'); + $this->invoke($controller, 'respondCreated', [['id' => 3], 'A Custom Reason']); + $this->assertStringStartsWith(config('Format')->supportedResponseFormats[0], $response->getHeaderLine('Content-Type')); } public function testResponseFormat() { - $data = ['foo' => 'something']; - + $data = ['foo' => 'something']; $controller = $this->makeController(); - $controller->setResponseFormat('json'); - $controller->respond($data, 201); + + $this->invoke($controller, 'setResponseFormat', ['json']); + $this->invoke($controller, 'respond', [$data, 201]); $this->assertStringStartsWith('application/json', $this->response->getHeaderLine('Content-Type')); $this->assertSame($this->formatter->format($data), $this->response->getJSON()); - $controller->setResponseFormat('xml'); - $controller->respond($data, 201); + $this->invoke($controller, 'setResponseFormat', ['xml']); + $this->invoke($controller, 'respond', [$data, 201]); $this->assertStringStartsWith('application/xml', $this->response->getHeaderLine('Content-Type')); } @@ -566,10 +584,18 @@ public function testXMLResponseFormat() $data = ['foo' => 'bar']; $controller = $this->makeController(); $controller->resetFormatter(); - $controller->setResponseFormat('xml'); - $controller->respond($data, 201); + + $this->invoke($controller, 'setResponseFormat', ['xml']); + $this->invoke($controller, 'respond', [$data, 201]); $xmlFormatter = new XMLFormatter(); $this->assertSame($xmlFormatter->format($data), $this->response->getXML()); } + + private function invoke(object $controller, string $method, array $args = []) + { + $method = $this->getPrivateMethodInvoker($controller, $method); + + return $method(...$args); + } } diff --git a/tests/system/RESTful/ResourceControllerTest.php b/tests/system/RESTful/ResourceControllerTest.php index ec8d13af7a2d..e6098527d09c 100644 --- a/tests/system/RESTful/ResourceControllerTest.php +++ b/tests/system/RESTful/ResourceControllerTest.php @@ -293,7 +293,7 @@ public function testJSONFormatOutput() 'foo' => 'bar', ]; - $theResponse = $resource->respond($data); + $theResponse = $this->invoke($resource, 'respond', [$data]); $result = $theResponse->getBody(); $JSONFormatter = new JSONFormatter(); @@ -321,7 +321,7 @@ public function testXMLFormatOutput() 'foo' => 'bar', ]; - $theResponse = $resource->respond($data); + $theResponse = $this->invoke($resource, 'respond', [$data]); $result = $theResponse->getBody(); $XMLFormatter = new XMLFormatter(); @@ -329,4 +329,11 @@ public function testXMLFormatOutput() $this->assertSame($expected, $result); } + + private function invoke(object $controller, string $method, array $args = []) + { + $method = $this->getPrivateMethodInvoker($controller, $method); + + return $method(...$args); + } } diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst index 74e782cb3508..4a48ea878d98 100644 --- a/user_guide_src/source/changelogs/index.rst +++ b/user_guide_src/source/changelogs/index.rst @@ -12,6 +12,7 @@ See all the changes. .. toctree:: :titlesonly: + v4.1.8 v4.1.7 v4.1.6 v4.1.5 diff --git a/user_guide_src/source/changelogs/v4.1.8.rst b/user_guide_src/source/changelogs/v4.1.8.rst new file mode 100644 index 000000000000..2d6c7c8c0ea5 --- /dev/null +++ b/user_guide_src/source/changelogs/v4.1.8.rst @@ -0,0 +1,15 @@ +Version 4.1.8 +############# + +Release Date: January 24, 2022 + +**4.1.8 release of CodeIgniter4** + +.. contents:: + :local: + :depth: 2 + +SECURITY +******** + +- *XSS Vulnerability* in the ``API\ResponseTrait`` was fixed. See the `Security advisory `_ for more information. diff --git a/user_guide_src/source/conf.py b/user_guide_src/source/conf.py index 60a34e9c178b..0f7585e66df4 100644 --- a/user_guide_src/source/conf.py +++ b/user_guide_src/source/conf.py @@ -24,7 +24,7 @@ version = '4.1' # The full version, including alpha/beta/rc tags. -release = '4.1.7' +release = '4.1.8' # -- General configuration --------------------------------------------------- diff --git a/user_guide_src/source/installation/upgrade_418.rst b/user_guide_src/source/installation/upgrade_418.rst new file mode 100644 index 000000000000..ee3973ba37bc --- /dev/null +++ b/user_guide_src/source/installation/upgrade_418.rst @@ -0,0 +1,18 @@ +############################# +Upgrading from 4.1.7 to 4.1.8 +############################# + +Please refer to the upgrade instructions corresponding to your installation method. + +- :ref:`Composer Installation App Starter Upgrading ` +- :ref:`Composer Installation Adding CodeIgniter4 to an Existing Project Upgrading ` +- :ref:`Manual Installation Upgrading ` + +.. contents:: + :local: + :depth: 2 + +Breaking Changes +**************** + +- Due to a security issue in the ``API\ResponseTrait`` all trait methods are now scoped to ``protected``. See the `Security advisory` ` for more information.