Merge remote-tracking branch 'origin/develop' into 4.5

Conflicts:
	system/CodeIgniter.php
	system/Filters/Filters.php
	system/Language/Language.php
	system/Router/Router.php
This commit is contained in:
kenjis 2024-03-29 13:56:45 +09:00
commit 96ce795691
No known key found for this signature in database
GPG Key ID: BD254878922AF198
19 changed files with 380 additions and 46 deletions

View File

@ -1,5 +1,38 @@
# Changelog
## [v4.4.7](https://github.com/codeigniter4/CodeIgniter4/tree/v4.4.7) (2024-03-29)
[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.4.6...v4.4.7)
### SECURITY
* **Language:** *Language class DoS Vulnerability* was fixed. See the
[Security advisory](https://github.com/codeigniter4/CodeIgniter4/security/advisories/GHSA-39fp-mqmm-gxj6)
for more information.
* **URI Security:** The feature to check if URIs do not contain not permitted
strings has been added. This check is equivalent to the URI Security found in
CodeIgniter 3. This is enabled by default, but upgraded users need to add
a setting to enable it.
* **Filters:** A bug where URI paths processed by Filters were not URL-decoded
has been fixed.
### Breaking Changes
* fix: Time::difference() DST bug by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8661
### Fixed Bugs
* fix: [Validation] FileRules cause error if getimagesize() returns false by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8592
* fix: isWriteType() to recognize CTE; always excluding RETURNING by @markconnellypro in https://github.com/codeigniter4/CodeIgniter4/pull/8599
* fix: duplicate Cache-Control header with Session by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8601
* fix: [DebugBar] scroll to top by @ddevsr in https://github.com/codeigniter4/CodeIgniter4/pull/8595
* fix: Model::shouldUpdate() logic by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8614
* fix: esc() for 'raw' context by @Cleric-K in https://github.com/codeigniter4/CodeIgniter4/pull/8633
* docs: fix incorrect CURLRequest allow_redirects description by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8653
* fix: Model::set() does not accept object by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8670
### Refactoring
* refactor: replace PHP_VERSION by PHP_VERSION_ID by @justbyitself in https://github.com/codeigniter4/CodeIgniter4/pull/8618
* refactor: apply early return pattern by @justbyitself in https://github.com/codeigniter4/CodeIgniter4/pull/8621
* refactor: move footer info to top in error_exception.php by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/8626
## [v4.4.6](https://github.com/codeigniter4/CodeIgniter4/tree/v4.4.6) (2024-02-24)
[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.4.5...v4.4.6)

View File

@ -59,6 +59,30 @@ class App extends BaseConfig
*/
public string $uriProtocol = 'REQUEST_URI';
/*
|--------------------------------------------------------------------------
| Allowed URL Characters
|--------------------------------------------------------------------------
|
| This lets you specify which characters are permitted within your URLs.
| When someone tries to submit a URL with disallowed characters they will
| get a warning message.
|
| As a security measure you are STRONGLY encouraged to restrict URLs to
| as few characters as possible.
|
| By default, only these are allowed: `a-z 0-9~%.:_-`
|
| Set an empty string to allow all characters -- but only if you are insane.
|
| The configured value is actually a regular expression character group
| and it will be used as: '/\A[<permittedURIChars>]+\z/iu'
|
| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
|
*/
public string $permittedURIChars = 'a-z 0-9~%.:_\-';
/**
* --------------------------------------------------------------------------
* Default Locale

View File

@ -10,7 +10,7 @@
<output>api/build/</output>
<cache>api/cache/</cache>
</paths>
<version number="4.4.6">
<version number="4.4.7">
<api format="php">
<source dsn=".">
<path>system</path>

View File

@ -13428,7 +13428,7 @@ $ignoreErrors[] = [
];
$ignoreErrors[] = [
'message' => '#^Assigning \'GET\' directly on offset \'REQUEST_METHOD\' of \\$_SERVER is discouraged\\.$#',
'count' => 35,
'count' => 36,
'path' => __DIR__ . '/tests/system/Filters/FiltersTest.php',
];
$ignoreErrors[] = [

View File

@ -56,7 +56,7 @@ class CodeIgniter
/**
* The current version of CodeIgniter Framework
*/
public const CI_VERSION = '4.4.6';
public const CI_VERSION = '4.4.7';
/**
* App startup time.
@ -456,6 +456,7 @@ class CodeIgniter
$routeFilters = $this->tryToRouteIt($routes);
// $uri is URL-encoded.
$uri = $this->request->getPath();
if ($this->enableFilters) {
@ -823,6 +824,7 @@ class CodeIgniter
// $routes is defined in Config/Routes.php
$this->router = Services::router($routes, $this->request);
// $uri is URL-encoded.
$uri = $this->request->getPath();
$this->outputBufferingStart();

View File

@ -384,6 +384,9 @@ class Filters
return $this;
}
// Decode URL-encoded string
$uri = urldecode($uri);
$oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false;
if ($oldFilterOrder) {
$this->processGlobals($uri);
@ -841,7 +844,7 @@ class Filters
/**
* Check the URI path as pseudo-regex
*
* @param string $uri URI path relative to baseURL (all lowercase)
* @param string $uri URI path relative to baseURL (all lowercase, URL-decoded)
* @param array $paths The except path patterns
*/
private function checkPseudoRegex(string $uri, array $paths): bool
@ -854,7 +857,7 @@ class Filters
$path = strtolower(str_replace('*', '.*', $path));
// Does this rule apply here?
if (preg_match('#^' . $path . '$#', $uri, $match) === 1) {
if (preg_match('#\A' . $path . '\z#u', $uri, $match) === 1) {
return true;
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\HTTP\Exceptions;
use CodeIgniter\Exceptions\HTTPExceptionInterface;
use RuntimeException;
/**
* 400 Bad Request
*/
class BadRequestException extends RuntimeException implements HTTPExceptionInterface
{
/**
* HTTP status code for Bad Request
*
* @var int
*/
protected $code = 400; // @phpstan-ignore-line
}

View File

@ -13,7 +13,7 @@ declare(strict_types=1);
namespace CodeIgniter\Language;
use InvalidArgumentException;
use IntlException;
use MessageFormatter;
/**
@ -195,9 +195,33 @@ class Language
$formatted = MessageFormatter::formatMessage($this->locale, $message, $args);
if ($formatted === false) {
throw new InvalidArgumentException(
lang('Language.invalidMessageFormat', [$message, implode(',', $args)])
// Format again to get the error message.
try {
$fmt = new MessageFormatter($this->locale, $message);
$formatted = $fmt->format($args);
$fmtError = '"' . $fmt->getErrorMessage() . '" (' . $fmt->getErrorCode() . ')';
} catch (IntlException $e) {
$fmtError = '"' . $e->getMessage() . '" (' . $e->getCode() . ')';
}
$argsString = implode(
', ',
array_map(static fn ($element) => '"' . $element . '"', $args)
);
$argsUrlEncoded = implode(
', ',
array_map(static fn ($element) => '"' . rawurlencode($element) . '"', $args)
);
log_message(
'error',
'Language.invalidMessageFormat: $message: "' . $message
. '", $args: ' . $argsString
. ' (urlencoded: ' . $argsUrlEncoded . '),'
. ' MessageFormatter Error: ' . $fmtError
);
return $message . "\n【Warning】Also, invalid string(s) was passed to the Language class. See log file for details.";
}
return $formatted;

View File

@ -15,6 +15,7 @@ namespace CodeIgniter\Router;
use Closure;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\Exceptions\BadRequestException;
use CodeIgniter\HTTP\Exceptions\RedirectException;
use CodeIgniter\HTTP\Method;
use CodeIgniter\HTTP\Request;
@ -130,11 +131,23 @@ class Router implements RouterInterface
protected ?AutoRouterInterface $autoRouter = null;
/**
* Permitted URI chars
*
* The default value is `''` (do not check) for backward compatibility.
*/
protected string $permittedURIChars = '';
/**
* Stores a reference to the RouteCollection object.
*/
public function __construct(RouteCollectionInterface $routes, ?Request $request = null)
{
$config = config(App::class);
if (isset($config->permittedURIChars)) {
$this->permittedURIChars = $config->permittedURIChars;
}
$this->collection = $routes;
// These are only for auto-routing
@ -187,6 +200,8 @@ class Router implements RouterInterface
// Decode URL-encoded string
$uri = urldecode($uri);
$this->checkDisallowedChars($uri);
// Restart filterInfo
$this->filtersInfo = [];
@ -424,7 +439,7 @@ class Router implements RouterInterface
}, is_array($handler) ? key($handler) : $handler);
throw new RedirectException(
preg_replace('#^' . $routeKey . '$#u', $redirectTo, $uri),
preg_replace('#\A' . $routeKey . '\z#u', $redirectTo, $uri),
$this->collection->getRedirectCode($routeKey)
);
}
@ -484,7 +499,7 @@ class Router implements RouterInterface
if (config(Routing::class)->multipleSegmentsOneParam === false) {
// Using back-references
$segments = explode('/', preg_replace('#^' . $routeKey . '$#u', $handler, $uri));
$segments = explode('/', preg_replace('#\A' . $routeKey . '\z#u', $handler, $uri));
} else {
if (str_contains($methodAndParams, '/')) {
[$method, $handlerParams] = explode('/', $methodAndParams, 2);
@ -708,4 +723,20 @@ class Router implements RouterInterface
$this->matchedRouteOptions = $this->collection->getRoutesOptions($route);
}
/**
* Checks disallowed characters
*/
private function checkDisallowedChars(string $uri): void
{
foreach (explode('/', $uri) as $segment) {
if ($segment !== '' && $this->permittedURIChars !== ''
&& preg_match('/\A[' . $this->permittedURIChars . ']+\z/iu', $segment) !== 1
) {
throw new BadRequestException(
'The URI you submitted has disallowed characters: "' . $segment . '"'
);
}
}
}
}

View File

@ -1058,6 +1058,52 @@ final class FiltersTest extends CIUnitTestCase
$this->assertSame($expected, $filters->initialize($uri)->getFilters());
}
public function testMatchesURIWithUnicode(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$config = [
'aliases' => [
'foo' => '',
'bar' => '',
'frak' => '',
'baz' => '',
],
'globals' => [
'before' => [
'foo' => ['except' => '日本語/*'],
'bar',
],
'after' => [
'foo' => ['except' => '日本語/*'],
'baz',
],
],
'filters' => [
'frak' => [
'before' => ['日本語/*'],
'after' => ['日本語/*'],
],
],
];
$filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
$filters = $this->createFilters($filtersConfig);
// URIs passed to Filters are URL-encoded.
$uri = '%E6%97%A5%E6%9C%AC%E8%AA%9E/foo/bar';
$expected = [
'before' => [
'bar',
'frak',
],
'after' => [
'baz',
'frak',
],
];
$this->assertSame($expected, $filters->initialize($uri)->getFilters());
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/1907
*/

View File

@ -475,8 +475,8 @@ final class URITest extends CIUnitTestCase
{
return [
'dot-segment' => [
'/./path/to/nowhere',
'/path/to/nowhere',
'/./path/to/nowhere', // path
'/path/to/nowhere', // expectedPath
],
'double-dots' => [
'/../path/to/nowhere',
@ -486,18 +486,30 @@ final class URITest extends CIUnitTestCase
'./path/to/nowhere',
'/path/to/nowhere',
],
'start-double' => [
'start-double-dot' => [
'../path/to/nowhere',
'/path/to/nowhere',
],
'decoded' => [
'../%41path',
'decode-percent-encoded-chars' => [
'/%41path',
'/Apath',
],
'encoded' => [
'decode-slash' => [
'/a%2Fb',
'/a/b',
],
'encode-unreserved-chars' => [
'/path^here',
'/path%5Ehere',
],
'encode-multibyte-chars' => [
'/あいう',
'/%E3%81%82%E3%81%84%E3%81%86',
],
'encode-invalid-percent-encoding' => [
'/pa%2-th',
'/pa%252-th',
],
];
}

View File

@ -16,7 +16,6 @@ namespace CodeIgniter\Language;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Mock\MockLanguage;
use Config\Services;
use InvalidArgumentException;
use MessageFormatter;
use Tests\Support\Language\SecondMockLanguage;
@ -139,18 +138,14 @@ final class LanguageTest extends CIUnitTestCase
$this->markTestSkipped('No intl support.');
}
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(
'Invalid message format: "تم الكشف عن كلمة المرور {0} بسبب اختراق البيانات وشوهدت {1 ، عدد} مرة في {2} في كلمات المرور المخترقة.", args: "password,hits,wording"'
);
$this->lang->setLocale('ar');
$this->lang->setData('Auth', [
'errorPasswordPwned' => 'تم الكشف عن كلمة المرور {0} بسبب اختراق البيانات وشوهدت {1 ، عدد} مرة في {2} في كلمات المرور المخترقة.',
]);
$line = 'تم الكشف عن كلمة المرور {0} بسبب اختراق البيانات وشوهدت {1 ، عدد} مرة في {2} في كلمات المرور المخترقة.';
$this->lang->setData('Auth', ['errorPasswordPwned' => $line]);
$this->lang->getLine('Auth.errorPasswordPwned', ['password', 'hits', 'wording']);
$output = $this->lang->getLine('Auth.errorPasswordPwned', ['password', 'hits', 'wording']);
$this->assertSame($line . "\n【Warning】Also, invalid string(s) was passed to the Language class. See log file for details.", $output);
}
/**

View File

@ -16,11 +16,13 @@ namespace CodeIgniter\Router;
use CodeIgniter\Config\Factories;
use CodeIgniter\Config\Services;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\Exceptions\BadRequestException;
use CodeIgniter\HTTP\Exceptions\RedirectException;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\Method;
use CodeIgniter\Router\Exceptions\RouterException;
use CodeIgniter\Test\CIUnitTestCase;
use Config\App;
use Config\Modules;
use Config\Routing;
use Tests\Support\Filters\Customfilter;
@ -96,6 +98,16 @@ final class RouterTest extends CIUnitTestCase
$router->handle('0');
}
public function testNotPermittedChars(): void
{
$router = new Router($this->collection, $this->request);
$this->expectException(BadRequestException::class);
$this->expectExceptionMessage('The URI you submitted has disallowed characters: "<a>"');
$router->handle('test/%3Ca%3E');
}
public function testURIMapsToController(): void
{
$router = new Router($this->collection, $this->request);
@ -817,6 +829,9 @@ final class RouterTest extends CIUnitTestCase
*/
public function testRegularExpressionWithUnicode(): void
{
$config = config(App::class);
$config->permittedURIChars = 'a-z 0-9~%.:_\-\x{0980}-\x{09ff}';
$this->collection->get('news/([a-z0-9\x{0980}-\x{09ff}-]+)', 'News::view/$1');
$router = new Router($this->collection, $this->request);
@ -836,6 +851,9 @@ final class RouterTest extends CIUnitTestCase
*/
public function testRegularExpressionPlaceholderWithUnicode(): void
{
$config = config(App::class);
$config->permittedURIChars = 'a-z 0-9~%.:_\-\x{0980}-\x{09ff}';
$this->collection->addPlaceholder('custom', '[a-z0-9\x{0980}-\x{09ff}-]+');
$this->collection->get('news/(:custom)', 'News::view/$1');

View File

@ -2,7 +2,7 @@
Version 4.4.7
#############
Release Date: Unreleased
Release Date: March 29, 2024
**4.4.7 release of CodeIgniter4**
@ -10,6 +10,20 @@ Release Date: Unreleased
:local:
:depth: 3
********
SECURITY
********
- **Language:** *Language class DoS Vulnerability* was fixed.
See the `Security advisory GHSA-39fp-mqmm-gxj6 <https://github.com/codeigniter4/CodeIgniter4/security/advisories/GHSA-39fp-mqmm-gxj6>`_
for more information.
- **URI Security:** The feature to check if URIs do not contain not permitted
strings has been added. This check is equivalent to the URI Security found in
CodeIgniter 3. This is enabled by default, but upgraded users need to add
a setting to enable it. See :ref:`urls-uri-security` for details.
- **Filters:** A bug where URI paths processed by Filters were not URL-decoded
has been fixed. See :ref:`upgrade-447-filter-paths` for details.
********
BREAKING
********
@ -19,18 +33,6 @@ BREAKING
hours due to Daylight Saving Time (DST). This bug has been fixed. See
:ref:`Note in Times and Dates <time-viewing-differences>` for details.
***************
Message Changes
***************
*******
Changes
*******
************
Deprecations
************
**********
Bugs Fixed
**********

View File

@ -38,6 +38,7 @@ OWASP recommendations
CodeIgniter provisions
======================
- :ref:`urls-uri-security`
- :ref:`invalidchars` filter
- :doc:`../libraries/validation` library
- :doc:`HTTP library <../incoming/incomingrequest>` provides for :ref:`input field filtering <incomingrequest-filtering-input-data>` & content metadata

View File

@ -26,7 +26,7 @@ copyright = '2019-' + str(year_now) + ' CodeIgniter Foundation'
version = '4.4'
# The full version, including alpha/beta/rc tags.
release = '4.4.6'
release = '4.4.7'
# -- General configuration ---------------------------------------------------

View File

@ -58,6 +58,52 @@ Route path /blog/news/2022/10 The URI path relative to the Bas
Query page=2
========== ==================================== =========================================
.. _urls-uri-security:
URI Security
============
.. versionadded:: 4.4.7
.. important::
Users upgrading from versions prior to v4.4.7 will need to add the following
to **app/Config/App.php** in order to use this feature::
public string $permittedURIChars = 'a-z 0-9~%.:_\-';
CodeIgniter is fairly restrictive regarding which characters it allows in your
URI strings (Route path) in order to help minimize the possibility that malicious
data can be passed to your application. URIs may only contain the following:
- Alpha-numeric text (latin characters only)
- Tilde: ``~``
- Percent sign: ``%``
- Period: ``.``
- Colon: ``:``
- Underscore: ``_``
- Dash: ``-``
- Space: `` ``
.. note::
This check is performed by the ``Router``. The Router takes the URL-encoded
value held by the ``SiteURI`` class, decodes it, and then checks that it
does not contain not permitted strings.
Adding Permitted Characters
---------------------------
The permitted characters can be changed by ``Config\App::$permittedURIChars``.
If you want to use Unicode for URI paths, modify it to allow the characters to
be used. For example, if you want to use Bengali, you will need to set the
following value in **app/Config/App.php**::
public string $permittedURIChars = 'a-z 0-9~%.:_\-\x{0980}-\x{09ff}';
A full list of Unicode ranges can be found at Wikipedia's `Unicode block`_.
.. _Unicode block: https://en.wikipedia.org/wiki/Unicode_block
.. _urls-remove-index-php:
Removing the index.php file

View File

@ -163,6 +163,11 @@ an array with the ``except`` key and a URI path (relative to BaseURL) to match a
.. literalinclude:: filters/006.php
.. Warning:: Prior to v4.4.7, due to a bug, the URI paths processed by the filter
were not URL-decoded. In other words, the URI paths specified in the routing
and the URI paths specified in the filter could be different.
See :ref:`upgrade-447-filter-paths` for details.
Any place you can use a URI path (relative to BaseURL) in the filter settings, you can use a regular expression or, like in this example, use
an asterisk (``*``) for a wildcard that will match all characters after that. In this example, any URI path starting with ``api/``
would be exempted from CSRF protection, but the site's forms would all be protected.
@ -198,6 +203,11 @@ a list of URI path (relative to BaseURL) patterns that filter should apply to:
.. literalinclude:: filters/009.php
.. Warning:: Prior to v4.4.7, due to a bug, the URI paths processed by the filter
were not URL-decoded. In other words, the URI paths specified in the routing
and the URI paths specified in the filter could be different.
See :ref:`upgrade-447-filter-paths` for details.
.. _filters-filters-filter-arguments:
Filter Arguments

View File

@ -16,6 +16,18 @@ Please refer to the upgrade instructions corresponding to your installation meth
Mandatory File Changes
**********************
URI Security
============
The feature to check if URIs do not contain not permitted strings has been added.
This check is equivalent to the URI Security found in CodeIgniter 3.
We recommend you enable this feature. Add the following to **app/Config/App.php**::
public string $permittedURIChars = 'a-z 0-9~%.:_\-';.
See :ref:`urls-uri-security` for details.
Error Files
===========
@ -28,6 +40,40 @@ The error page has been updated. Please update the following files:
Breaking Changes
****************
.. _upgrade-447-filter-paths:
Paths in Controller Filters
===========================
A bug where URI paths processed by :doc:`../incoming/filters` were not URL-decoded has been fixed.
.. note:: Note that :doc:`Router <../incoming/routing>` processes URL-decoded URI paths.
``Config\Filters`` has some places to specify the URI paths. If the paths have
different values when URL-decoded, change them to the URL-decoded values.
E.g.,:
.. code-block:: php
public array $globals = [
'before' => [
'csrf' => ['except' => '%E6%97%A5%E6%9C%AC%E8%AA%9E/*'],
],
// ...
];
.. code-block:: php
public array $globals = [
'before' => [
'csrf' => ['except' => '日本語/*'],
],
// ...
];
Time::difference() and DST
==========================
@ -43,10 +89,6 @@ In the unlikely event that you wish to maintain the behavior of the previous
versions, change the time zone of both dates being compared to UTC before passing
them to ``Time::difference()``.
*********************
Breaking Enhancements
*********************
*************
Project Files
*************
@ -66,7 +108,9 @@ and it is recommended that you merge the updated versions with your application:
Config
------
- @TODO
- app/Config/App.php
- The property ``$permittedURIChars`` was added. See :ref:`urls-uri-security`
for details.
All Changes
===========
@ -74,4 +118,19 @@ All Changes
This is a list of all files in the **project space** that received changes;
many will be simple comments or formatting that have no effect on the runtime:
- @TODO
- app/Config/Cache.php
- app/Config/ContentSecurityPolicy.php
- app/Config/Database.php
- app/Config/Exceptions.php
- app/Config/Filters.php
- app/Config/Format.php
- app/Config/Logger.php
- app/Config/Mimes.php
- app/Config/Routing.php
- app/Config/Toolbar.php
- app/Config/Validation.php
- app/Config/View.php
- app/Controllers/BaseController.php
- app/Views/errors/html/debug.css
- app/Views/errors/html/error_exception.php
- composer.json