From 449dbaebb7cf21fd5858467aa4329aa3932d96e6 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 11 Jun 2026 23:28:03 +0200 Subject: [PATCH 1/9] fix: env() TypeError for non-string $_SERVER values + esc() fixes - env(): guard non-string values (int argc, array argv in CLI) before strtolower() to prevent TypeError under declare(strict_types=1) - esc(): propagate $encoding in recursive array calls (was ignored before), add early return after array processing, replace single static $escaper with static $escapers[] cache keyed by encoding - tests: data-provider test for env() non-string types, three tests for esc() foreach reference leak --- system/Common.php | 23 ++++++---- tests/system/CommonFunctionsTest.php | 66 ++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/system/Common.php b/system/Common.php index ea0c476423a0..f6a876e7a732 100644 --- a/system/Common.php +++ b/system/Common.php @@ -416,6 +416,12 @@ function env(string $key, $default = null) return $default; } + // Non-string values (e.g. $_SERVER['argc'] is int, $_SERVER['argv'] is array in CLI) + // must be returned as-is to avoid TypeError from strtolower(). + if (! is_string($value)) { + return $value; + } + // Handle any boolean values return match (strtolower($value)) { 'true' => true, @@ -459,8 +465,11 @@ function esc($data, string $context = 'html', ?string $encoding = null) if (is_array($data)) { foreach ($data as &$value) { - $value = esc($value, $context); + $value = esc($value, $context, $encoding); } + unset($value); // Prevent reference leak: &$value would remain bound to last element + + return $data; } if (is_string($data)) { @@ -470,16 +479,14 @@ function esc($data, string $context = 'html', ?string $encoding = null) $method = $context === 'attr' ? 'escapeHtmlAttr' : 'escape' . ucfirst($context); - static $escaper; - if (! $escaper) { - $escaper = new Escaper($encoding); - } + static $escapers = []; + $cacheKey = $encoding ?? 'default'; - if ($encoding !== null && $escaper->getEncoding() !== $encoding) { - $escaper = new Escaper($encoding); + if (! isset($escapers[$cacheKey])) { + $escapers[$cacheKey] = new Escaper($encoding); } - $data = $escaper->{$method}($data); + $data = $escapers[$cacheKey]->{$method}($data); } return $data; diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index 7f79b07381fd..eddf703afc34 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -131,6 +131,42 @@ public function testEnvBooleans(): void $this->assertNull(env('p4')); } + #[DataProvider('provideEnvReturnsCorrectTypesWithoutTypeError')] + public function testEnvReturnsCorrectTypesWithoutTypeError(string $source, mixed $value): void + { + $key = 'ci_test_var'; + + if ($source === 'SERVER' || $source === 'BOTH') { + service('superglobals')->setServer($key, $value); + } + + if ($source === 'ENV' || $source === 'BOTH') { + $_ENV[$key] = $value; + } + + $this->assertSame($value, env($key)); + } + + /** + * @return iterable + */ + public static function provideEnvReturnsCorrectTypesWithoutTypeError(): iterable + { + yield 'integer from SERVER' => ['SERVER', 2]; + + yield 'array from SERVER' => ['SERVER', ['spark', 'migrate']]; + + yield 'int 1 is not true' => ['SERVER', 1]; + + yield 'int 0 is not false' => ['SERVER', 0]; + + yield 'float from SERVER' => ['SERVER', 3.14]; + + yield 'integer from ENV' => ['ENV', 42]; + + yield 'CLI simulation BOTH' => ['BOTH', 3]; + } + private function createRouteCollection(): RouteCollection { return new RouteCollection(Services::locator(), new Modules(), new Routing()); @@ -276,6 +312,36 @@ public function testEscapeRecursiveArrayRaw(): void $this->assertSame($data, esc($data, 'raw')); } + public function testEscapeArrayDoesNotLeakForeachReference(): void + { + $data = ['first' => 'bold', 'last' => 'italic']; + + $escaped = esc($data); + + $this->assertSame('<b>bold</b>', $escaped['first']); + $this->assertSame('<i>italic</i>', $escaped['last']); + } + + public function testEscapeArrayLastElementNotMutatedAfterCall(): void + { + $data = ['x' => '