diff --git a/src/Server/Session/FileSessionStore.php b/src/Server/Session/FileSessionStore.php index 9716e3d6..e218e75a 100644 --- a/src/Server/Session/FileSessionStore.php +++ b/src/Server/Session/FileSessionStore.php @@ -128,6 +128,11 @@ public function gc(): array continue; } + // Only delete files this store owns: sessions are named by their RFC 4122 UUID + if (!Uuid::isValid($entry)) { + continue; + } + $path = $this->directory.\DIRECTORY_SEPARATOR.$entry; if (!is_file($path)) { continue; @@ -136,11 +141,7 @@ public function gc(): array $mtime = @filemtime($path) ?: 0; if (($now - $mtime) > $this->ttl) { @unlink($path); - try { - $deleted[] = Uuid::fromString($entry); - } catch (\Throwable) { - // ignore non-UUID file names - } + $deleted[] = Uuid::fromString($entry); } } diff --git a/tests/Unit/Server/Session/FileSessionStoreTest.php b/tests/Unit/Server/Session/FileSessionStoreTest.php index 1e0caa75..a2b8729c 100644 --- a/tests/Unit/Server/Session/FileSessionStoreTest.php +++ b/tests/Unit/Server/Session/FileSessionStoreTest.php @@ -62,6 +62,28 @@ public function testWriteThenRead(): void $this->assertSame('payload', $store->read($id)); } + #[TestDox('gc() only deletes expired session files, never foreign files in the directory')] + public function testGcLeavesForeignFilesAlone(): void + { + $store = new FileSessionStore($this->directory, ttl: 60); + $id = new UuidV4(); + $store->write($id, 'payload'); + + $foreign = $this->directory.'/important.lock'; + file_put_contents($foreign, 'not a session'); + + // Make everything stale + $expired = time() - 120; + touch($this->directory.'/'.$id->toRfc4122(), $expired); + touch($foreign, $expired); + + $deleted = $store->gc(); + + $this->assertEquals([$id], $deleted); + $this->assertFileDoesNotExist($this->directory.'/'.$id->toRfc4122()); + $this->assertFileExists($foreign); + } + #[TestDox('rejects an unwritable directory with the SDK\'s own exception')] public function testUnwritableDirectoryThrowsPackageException(): void {