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.