From 62be79a1c5e8b3eeeaa363a7718287450f6911f1 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Sun, 19 Apr 2026 16:24:37 +0100 Subject: [PATCH] feature: escape filename --- src/FileAccess.php | 13 +++++++++---- test/phpunit/CacheTest.php | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/FileAccess.php b/src/FileAccess.php index e61cb59..bf32467 100644 --- a/src/FileAccess.php +++ b/src/FileAccess.php @@ -8,7 +8,7 @@ public function __construct( } public function getData(string $name):mixed { - $filePath = "$this->dirPath/$name"; + $filePath = $this->getFilePath($name); if(!is_file($filePath)) { throw new FileNotFoundException($filePath); } @@ -18,7 +18,7 @@ public function getData(string $name):mixed { } public function setData(string $name, mixed $value):void { - $filePath = "$this->dirPath/$name"; + $filePath = $this->getFilePath($name); if(!is_dir(dirname($filePath))) { mkdir(dirname($filePath), 0775, true); } @@ -26,7 +26,7 @@ public function setData(string $name, mixed $value):void { } public function checkValidity(string $name, int $secondsValidity):void { - $filePath = "$this->dirPath/$name"; + $filePath = $this->getFilePath($name); if(!is_file($filePath)) { throw new CacheInvalidException("$filePath (does not exist)"); } @@ -37,11 +37,16 @@ public function checkValidity(string $name, int $secondsValidity):void { } public function invalidate(string $name):void { - $filePath = "$this->dirPath/$name"; + $filePath = $this->getFilePath($name); if(!is_file($filePath)) { return; } unlink($filePath); } + + private function getFilePath(string $name):string { + $escapedName = rawurlencode($name); + return "$this->dirPath/$escapedName"; + } } diff --git a/test/phpunit/CacheTest.php b/test/phpunit/CacheTest.php index d6d8f75..0a06554 100644 --- a/test/phpunit/CacheTest.php +++ b/test/phpunit/CacheTest.php @@ -66,6 +66,33 @@ public function testGet_nullValueCanBeCached():void { self::assertSame(1, $count); } + public function testGet_urlName_isEscapedToReadableFilename():void { + $sut = $this->getSut(); + $name = "https://example.com/test"; + $value = "cached-value"; + + self::assertSame($value, $sut->get($name, fn() => $value)); + + $expectedFile = sys_get_temp_dir() + . "/phpgt-filecache/" + . rawurlencode($name); + self::assertFileExists($expectedFile); + self::assertSame($value, unserialize(file_get_contents($expectedFile))); + } + + public function testGet_urlName_doesNotTraverseFilesystem():void { + $sut = $this->getSut(); + $name = "../outside-cache"; + $value = "cached-value"; + + self::assertSame($value, $sut->get($name, fn() => $value)); + + $cacheDir = sys_get_temp_dir() . "/phpgt-filecache"; + $expectedFile = $cacheDir . "/" . rawurlencode($name); + self::assertFileExists($expectedFile); + self::assertFileDoesNotExist(sys_get_temp_dir() . "/outside-cache"); + } + public function testGet_generationExceptionDoesNotWriteInvalidValue():void { $fileAccess = self::createMock(FileAccess::class); $fileAccess->expects(self::once())