Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions system/Cache/Handlers/FileHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
52 changes: 52 additions & 0 deletions tests/system/Cache/Handlers/FileHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string, array{string}>
*/
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'));
}
}
Loading