diff --git a/system/Cache/Handlers/FileHandler.php b/system/Cache/Handlers/FileHandler.php index 9c00998c1f85..c11f44a9224a 100644 --- a/system/Cache/Handlers/FileHandler.php +++ b/system/Cache/Handlers/FileHandler.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Cache\Handlers; use CodeIgniter\Cache\Exceptions\CacheException; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\I18n\Time; use Config\Cache; use FilesystemIterator; @@ -76,6 +77,32 @@ public function initialize(): void { } + /** + * Validates a cache key, ensuring it contains no directory traversal sequences + * or path separators, regardless of the configured reservedCharacters setting. + * + * @param mixed $key The key to validate + * @param string $prefix Optional prefix to include in length calculations + * + * @throws InvalidArgumentException When $key is not valid or contains path separators + */ + public static function validateKey($key, $prefix = ''): string + { + if (! is_string($key)) { + throw new InvalidArgumentException('Cache key must be a string'); + } + + if (strpbrk($key, "/\\\0") !== false || str_contains($key, '..')) { + throw new InvalidArgumentException('Cache key cannot contain directory separators or traversal sequences.'); + } + + if (strpbrk($prefix, "/\\\0") !== false || str_contains($prefix, '..')) { + throw new InvalidArgumentException('Cache prefix cannot contain directory separators or traversal sequences.'); + } + + return parent::validateKey($key, $prefix); + } + public function get(string $key): mixed { $key = static::validateKey($key, $this->prefix); diff --git a/tests/system/Cache/Handlers/FileHandlerTest.php b/tests/system/Cache/Handlers/FileHandlerTest.php index 310b5a7ebce7..a656b295fddf 100644 --- a/tests/system/Cache/Handlers/FileHandlerTest.php +++ b/tests/system/Cache/Handlers/FileHandlerTest.php @@ -16,6 +16,7 @@ use CodeIgniter\Cache\CacheFactory; use CodeIgniter\Cache\Exceptions\CacheException; use CodeIgniter\CLI\CLI; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\I18n\Time; use Config\Cache; use PHPUnit\Framework\Attributes\DataProvider; @@ -504,4 +505,55 @@ public function testReconnect(): void { $this->assertTrue($this->handler->reconnect()); } + + #[DataProvider('provideValidateKeyRejectsTraversalAndSeparatorsEvenWithEmptyReservedCharacters')] + public function testValidateKeyRejectsTraversalAndSeparatorsEvenWithEmptyReservedCharacters(string $key): void + { + $config = config('Cache'); + $originalReservedCharacters = $config->reservedCharacters; + + // Multi-store setups often empty this setting to share keys between + // handlers, which must not weaken the file handler's own validation. + $config->reservedCharacters = ''; + + try { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Cache key cannot contain directory separators or traversal sequences.'); + + FileHandler::validateKey($key); + } finally { + $config->reservedCharacters = $originalReservedCharacters; + } + } + + /** + * @return iterable + */ + public static function provideValidateKeyRejectsTraversalAndSeparatorsEvenWithEmptyReservedCharacters(): iterable + { + yield 'parent directory' => ['../evil']; + + yield 'nested traversal' => ['sub/../../evil']; + + yield 'forward slash separator' => ['sub/key']; + + yield 'backslash separator' => ['sub\\key']; + + yield 'parent traversal only' => ['..']; + + yield 'null byte' => ["sub\0key"]; + } + + public function testValidateKeyRejectsTraversalInPrefix(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Cache prefix cannot contain directory separators or traversal sequences.'); + + FileHandler::validateKey('valid_key', '../'); + } + + public function testValidateKeySuccess(): void + { + $this->assertSame('valid_key', FileHandler::validateKey('valid_key')); + } }