Skip to content

Commit 96ce795

Browse files
committed
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
2 parents 5150c45 + 3503b4d commit 96ce795

File tree

19 files changed

+380
-46
lines changed

19 files changed

+380
-46
lines changed

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,38 @@
11
# Changelog
22

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

app/Config/App.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,30 @@ class App extends BaseConfig
5959
*/
6060
public string $uriProtocol = 'REQUEST_URI';
6161

62+
/*
63+
|--------------------------------------------------------------------------
64+
| Allowed URL Characters
65+
|--------------------------------------------------------------------------
66+
|
67+
| This lets you specify which characters are permitted within your URLs.
68+
| When someone tries to submit a URL with disallowed characters they will
69+
| get a warning message.
70+
|
71+
| As a security measure you are STRONGLY encouraged to restrict URLs to
72+
| as few characters as possible.
73+
|
74+
| By default, only these are allowed: `a-z 0-9~%.:_-`
75+
|
76+
| Set an empty string to allow all characters -- but only if you are insane.
77+
|
78+
| The configured value is actually a regular expression character group
79+
| and it will be used as: '/\A[<permittedURIChars>]+\z/iu'
80+
|
81+
| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
82+
|
83+
*/
84+
public string $permittedURIChars = 'a-z 0-9~%.:_\-';
85+
6286
/**
6387
* --------------------------------------------------------------------------
6488
* Default Locale

phpdoc.dist.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<output>api/build/</output>
1111
<cache>api/cache/</cache>
1212
</paths>
13-
<version number="4.4.6">
13+
<version number="4.4.7">
1414
<api format="php">
1515
<source dsn=".">
1616
<path>system</path>

phpstan-baseline.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13428,7 +13428,7 @@
1342813428
];
1342913429
$ignoreErrors[] = [
1343013430
'message' => '#^Assigning \'GET\' directly on offset \'REQUEST_METHOD\' of \\$_SERVER is discouraged\\.$#',
13431-
'count' => 35,
13431+
'count' => 36,
1343213432
'path' => __DIR__ . '/tests/system/Filters/FiltersTest.php',
1343313433
];
1343413434
$ignoreErrors[] = [

system/CodeIgniter.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class CodeIgniter
5656
/**
5757
* The current version of CodeIgniter Framework
5858
*/
59-
public const CI_VERSION = '4.4.6';
59+
public const CI_VERSION = '4.4.7';
6060

6161
/**
6262
* App startup time.
@@ -456,6 +456,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
456456

457457
$routeFilters = $this->tryToRouteIt($routes);
458458

459+
// $uri is URL-encoded.
459460
$uri = $this->request->getPath();
460461

461462
if ($this->enableFilters) {
@@ -823,6 +824,7 @@ protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
823824
// $routes is defined in Config/Routes.php
824825
$this->router = Services::router($routes, $this->request);
825826

827+
// $uri is URL-encoded.
826828
$uri = $this->request->getPath();
827829

828830
$this->outputBufferingStart();

system/Filters/Filters.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,9 @@ public function initialize(?string $uri = null)
384384
return $this;
385385
}
386386

387+
// Decode URL-encoded string
388+
$uri = urldecode($uri);
389+
387390
$oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false;
388391
if ($oldFilterOrder) {
389392
$this->processGlobals($uri);
@@ -841,7 +844,7 @@ private function checkExcept(string $uri, $paths): bool
841844
/**
842845
* Check the URI path as pseudo-regex
843846
*
844-
* @param string $uri URI path relative to baseURL (all lowercase)
847+
* @param string $uri URI path relative to baseURL (all lowercase, URL-decoded)
845848
* @param array $paths The except path patterns
846849
*/
847850
private function checkPseudoRegex(string $uri, array $paths): bool
@@ -854,7 +857,7 @@ private function checkPseudoRegex(string $uri, array $paths): bool
854857
$path = strtolower(str_replace('*', '.*', $path));
855858

856859
// Does this rule apply here?
857-
if (preg_match('#^' . $path . '$#', $uri, $match) === 1) {
860+
if (preg_match('#\A' . $path . '\z#u', $uri, $match) === 1) {
858861
return true;
859862
}
860863
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/**
4+
* This file is part of CodeIgniter 4 framework.
5+
*
6+
* (c) CodeIgniter Foundation <[email protected]>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace CodeIgniter\HTTP\Exceptions;
13+
14+
use CodeIgniter\Exceptions\HTTPExceptionInterface;
15+
use RuntimeException;
16+
17+
/**
18+
* 400 Bad Request
19+
*/
20+
class BadRequestException extends RuntimeException implements HTTPExceptionInterface
21+
{
22+
/**
23+
* HTTP status code for Bad Request
24+
*
25+
* @var int
26+
*/
27+
protected $code = 400; // @phpstan-ignore-line
28+
}

system/Language/Language.php

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
namespace CodeIgniter\Language;
1515

16-
use InvalidArgumentException;
16+
use IntlException;
1717
use MessageFormatter;
1818

1919
/**
@@ -195,9 +195,33 @@ protected function formatMessage($message, array $args = [])
195195

196196
$formatted = MessageFormatter::formatMessage($this->locale, $message, $args);
197197
if ($formatted === false) {
198-
throw new InvalidArgumentException(
199-
lang('Language.invalidMessageFormat', [$message, implode(',', $args)])
198+
// Format again to get the error message.
199+
try {
200+
$fmt = new MessageFormatter($this->locale, $message);
201+
$formatted = $fmt->format($args);
202+
$fmtError = '"' . $fmt->getErrorMessage() . '" (' . $fmt->getErrorCode() . ')';
203+
} catch (IntlException $e) {
204+
$fmtError = '"' . $e->getMessage() . '" (' . $e->getCode() . ')';
205+
}
206+
207+
$argsString = implode(
208+
', ',
209+
array_map(static fn ($element) => '"' . $element . '"', $args)
210+
);
211+
$argsUrlEncoded = implode(
212+
', ',
213+
array_map(static fn ($element) => '"' . rawurlencode($element) . '"', $args)
200214
);
215+
216+
log_message(
217+
'error',
218+
'Language.invalidMessageFormat: $message: "' . $message
219+
. '", $args: ' . $argsString
220+
. ' (urlencoded: ' . $argsUrlEncoded . '),'
221+
. ' MessageFormatter Error: ' . $fmtError
222+
);
223+
224+
return $message . "\n【Warning】Also, invalid string(s) was passed to the Language class. See log file for details.";
201225
}
202226

203227
return $formatted;

system/Router/Router.php

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use Closure;
1717
use CodeIgniter\Exceptions\PageNotFoundException;
18+
use CodeIgniter\HTTP\Exceptions\BadRequestException;
1819
use CodeIgniter\HTTP\Exceptions\RedirectException;
1920
use CodeIgniter\HTTP\Method;
2021
use CodeIgniter\HTTP\Request;
@@ -130,11 +131,23 @@ class Router implements RouterInterface
130131

131132
protected ?AutoRouterInterface $autoRouter = null;
132133

134+
/**
135+
* Permitted URI chars
136+
*
137+
* The default value is `''` (do not check) for backward compatibility.
138+
*/
139+
protected string $permittedURIChars = '';
140+
133141
/**
134142
* Stores a reference to the RouteCollection object.
135143
*/
136144
public function __construct(RouteCollectionInterface $routes, ?Request $request = null)
137145
{
146+
$config = config(App::class);
147+
if (isset($config->permittedURIChars)) {
148+
$this->permittedURIChars = $config->permittedURIChars;
149+
}
150+
138151
$this->collection = $routes;
139152

140153
// These are only for auto-routing
@@ -187,6 +200,8 @@ public function handle(?string $uri = null)
187200
// Decode URL-encoded string
188201
$uri = urldecode($uri);
189202

203+
$this->checkDisallowedChars($uri);
204+
190205
// Restart filterInfo
191206
$this->filtersInfo = [];
192207

@@ -424,7 +439,7 @@ protected function checkRoutes(string $uri): bool
424439
}, is_array($handler) ? key($handler) : $handler);
425440

426441
throw new RedirectException(
427-
preg_replace('#^' . $routeKey . '$#u', $redirectTo, $uri),
442+
preg_replace('#\A' . $routeKey . '\z#u', $redirectTo, $uri),
428443
$this->collection->getRedirectCode($routeKey)
429444
);
430445
}
@@ -484,7 +499,7 @@ protected function checkRoutes(string $uri): bool
484499

485500
if (config(Routing::class)->multipleSegmentsOneParam === false) {
486501
// Using back-references
487-
$segments = explode('/', preg_replace('#^' . $routeKey . '$#u', $handler, $uri));
502+
$segments = explode('/', preg_replace('#\A' . $routeKey . '\z#u', $handler, $uri));
488503
} else {
489504
if (str_contains($methodAndParams, '/')) {
490505
[$method, $handlerParams] = explode('/', $methodAndParams, 2);
@@ -708,4 +723,20 @@ protected function setMatchedRoute(string $route, $handler): void
708723

709724
$this->matchedRouteOptions = $this->collection->getRoutesOptions($route);
710725
}
726+
727+
/**
728+
* Checks disallowed characters
729+
*/
730+
private function checkDisallowedChars(string $uri): void
731+
{
732+
foreach (explode('/', $uri) as $segment) {
733+
if ($segment !== '' && $this->permittedURIChars !== ''
734+
&& preg_match('/\A[' . $this->permittedURIChars . ']+\z/iu', $segment) !== 1
735+
) {
736+
throw new BadRequestException(
737+
'The URI you submitted has disallowed characters: "' . $segment . '"'
738+
);
739+
}
740+
}
741+
}
711742
}

tests/system/Filters/FiltersTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,52 @@ public function testMatchesURICaseInsensitively(): void
10581058
$this->assertSame($expected, $filters->initialize($uri)->getFilters());
10591059
}
10601060

1061+
public function testMatchesURIWithUnicode(): void
1062+
{
1063+
$_SERVER['REQUEST_METHOD'] = 'GET';
1064+
1065+
$config = [
1066+
'aliases' => [
1067+
'foo' => '',
1068+
'bar' => '',
1069+
'frak' => '',
1070+
'baz' => '',
1071+
],
1072+
'globals' => [
1073+
'before' => [
1074+
'foo' => ['except' => '日本語/*'],
1075+
'bar',
1076+
],
1077+
'after' => [
1078+
'foo' => ['except' => '日本語/*'],
1079+
'baz',
1080+
],
1081+
],
1082+
'filters' => [
1083+
'frak' => [
1084+
'before' => ['日本語/*'],
1085+
'after' => ['日本語/*'],
1086+
],
1087+
],
1088+
];
1089+
$filtersConfig = $this->createConfigFromArray(FiltersConfig::class, $config);
1090+
$filters = $this->createFilters($filtersConfig);
1091+
1092+
// URIs passed to Filters are URL-encoded.
1093+
$uri = '%E6%97%A5%E6%9C%AC%E8%AA%9E/foo/bar';
1094+
$expected = [
1095+
'before' => [
1096+
'bar',
1097+
'frak',
1098+
],
1099+
'after' => [
1100+
'baz',
1101+
'frak',
1102+
],
1103+
];
1104+
$this->assertSame($expected, $filters->initialize($uri)->getFilters());
1105+
}
1106+
10611107
/**
10621108
* @see https://github.com/codeigniter4/CodeIgniter4/issues/1907
10631109
*/

tests/system/HTTP/URITest.php

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -475,8 +475,8 @@ public static function providePathGetsFiltered(): iterable
475475
{
476476
return [
477477
'dot-segment' => [
478-
'/./path/to/nowhere',
479-
'/path/to/nowhere',
478+
'/./path/to/nowhere', // path
479+
'/path/to/nowhere', // expectedPath
480480
],
481481
'double-dots' => [
482482
'/../path/to/nowhere',
@@ -486,18 +486,30 @@ public static function providePathGetsFiltered(): iterable
486486
'./path/to/nowhere',
487487
'/path/to/nowhere',
488488
],
489-
'start-double' => [
489+
'start-double-dot' => [
490490
'../path/to/nowhere',
491491
'/path/to/nowhere',
492492
],
493-
'decoded' => [
494-
'../%41path',
493+
'decode-percent-encoded-chars' => [
494+
'/%41path',
495495
'/Apath',
496496
],
497-
'encoded' => [
497+
'decode-slash' => [
498+
'/a%2Fb',
499+
'/a/b',
500+
],
501+
'encode-unreserved-chars' => [
498502
'/path^here',
499503
'/path%5Ehere',
500504
],
505+
'encode-multibyte-chars' => [
506+
'/あいう',
507+
'/%E3%81%82%E3%81%84%E3%81%86',
508+
],
509+
'encode-invalid-percent-encoding' => [
510+
'/pa%2-th',
511+
'/pa%252-th',
512+
],
501513
];
502514
}
503515

0 commit comments

Comments
 (0)