Skip to content

Commit 8f13b09

Browse files
committed
fix: resolve TOCTOU symlink vulnerability in FileHandler gc and destroy
1 parent 1bee883 commit 8f13b09

2 files changed

Lines changed: 121 additions & 10 deletions

File tree

system/Session/Handlers/FileHandler.php

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -240,17 +240,19 @@ public function close(): bool
240240
*/
241241
public function destroy($id): bool
242242
{
243+
$filePath = $this->filePath . $id;
244+
243245
if ($this->close()) {
244-
return is_file($this->filePath . $id)
245-
? (unlink($this->filePath . $id) && $this->destroyCookie())
246+
return (! is_link($filePath) && is_file($filePath))
247+
? (@unlink($filePath) && $this->destroyCookie())
246248
: true;
247249
}
248250

249251
if ($this->filePath !== null) {
250252
clearstatcache();
251253

252-
return is_file($this->filePath . $id)
253-
? (unlink($this->filePath . $id) && $this->destroyCookie())
254+
return (! is_link($filePath) && is_file($filePath))
255+
? (@unlink($filePath) && $this->destroyCookie())
254256
: true;
255257
}
256258

@@ -284,15 +286,19 @@ public function gc($max_lifetime): false|int
284286

285287
while (($file = readdir($directory)) !== false) {
286288
// If the filename doesn't match this pattern, it's either not a session file or is not ours
287-
if (preg_match($pattern, $file) !== 1
288-
|| ! is_file($this->savePath . DIRECTORY_SEPARATOR . $file)
289-
|| ($mtime = filemtime($this->savePath . DIRECTORY_SEPARATOR . $file)) === false
290-
|| $mtime > $ts
291-
) {
289+
if (preg_match($pattern, $file) !== 1) {
290+
continue;
291+
}
292+
293+
$filePath = $this->savePath . DIRECTORY_SEPARATOR . $file;
294+
295+
$stat = @lstat($filePath);
296+
297+
if ($stat === false || is_link($filePath) || ! is_file($filePath) || $stat['mtime'] > $ts) {
292298
continue;
293299
}
294300

295-
unlink($this->savePath . DIRECTORY_SEPARATOR . $file);
301+
@unlink($filePath);
296302
$collected++;
297303
}
298304

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\Session\Handlers;
15+
16+
use CodeIgniter\Test\CIUnitTestCase;
17+
use Config\Session as SessionConfig;
18+
use PHPUnit\Framework\Attributes\Group;
19+
20+
/**
21+
* @internal
22+
*/
23+
#[Group('Others')]
24+
final class FileHandlerTest extends CIUnitTestCase
25+
{
26+
private string $savePath;
27+
28+
protected function setUp(): void
29+
{
30+
parent::setUp();
31+
32+
$this->savePath = WRITEPATH . 'session_test';
33+
if (! is_dir($this->savePath)) {
34+
mkdir($this->savePath, 0700, true);
35+
}
36+
}
37+
38+
protected function tearDown(): void
39+
{
40+
parent::tearDown();
41+
42+
// clean up
43+
if (is_dir($this->savePath)) {
44+
$files = array_diff(scandir($this->savePath), ['.', '..']);
45+
46+
foreach ($files as $file) {
47+
$path = $this->savePath . DIRECTORY_SEPARATOR . $file;
48+
if (is_link($path) || is_file($path)) {
49+
@unlink($path);
50+
}
51+
}
52+
rmdir($this->savePath);
53+
}
54+
}
55+
56+
public function testGcIgnoresSymlinks(): void
57+
{
58+
$config = new SessionConfig();
59+
$config->savePath = $this->savePath;
60+
$config->cookieName = 'ci_session';
61+
62+
$handler = new FileHandler($config, '127.0.0.1');
63+
64+
$sessionId = '1234567890abcdef1234567890abcdef';
65+
$sessionFile = $this->savePath . DIRECTORY_SEPARATOR . 'ci_session' . $sessionId;
66+
67+
$targetFile = $this->savePath . DIRECTORY_SEPARATOR . 'target.txt';
68+
file_put_contents($targetFile, 'target');
69+
touch($targetFile, time() - 10000);
70+
71+
symlink($targetFile, $sessionFile);
72+
73+
$this->assertTrue(is_link($sessionFile));
74+
75+
$collected = $handler->gc(5000);
76+
77+
$this->assertTrue(is_link($sessionFile));
78+
$this->assertSame(0, $collected);
79+
}
80+
81+
public function testDestroyIgnoresSymlinks(): void
82+
{
83+
$config = new SessionConfig();
84+
$config->savePath = $this->savePath;
85+
$config->cookieName = 'ci_session';
86+
87+
$handler = new FileHandler($config, '127.0.0.1');
88+
89+
$sessionId = '1234567890abcdef1234567890abcdef';
90+
$handler->open($this->savePath, 'ci_session');
91+
92+
$sessionFile = $this->savePath . DIRECTORY_SEPARATOR . 'ci_session' . $sessionId;
93+
94+
$targetFile = $this->savePath . DIRECTORY_SEPARATOR . 'target.txt';
95+
file_put_contents($targetFile, 'target');
96+
97+
symlink($targetFile, $sessionFile);
98+
99+
$this->assertTrue(is_link($sessionFile));
100+
101+
$result = $handler->destroy($sessionId);
102+
103+
$this->assertTrue(is_link($sessionFile));
104+
}
105+
}

0 commit comments

Comments
 (0)