Skip to content

Commit eecc9ed

Browse files
duncanmccleanclaudejasonvarga
authored
[5.x] Fix token path traversal (#14700)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Jason Varga <jason@pixelfear.com>
1 parent 68d2eab commit eecc9ed

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

src/Tokens/FileTokenRepository.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,19 @@ class FileTokenRepository extends TokenRepository
1111
{
1212
public function make(?string $token, string $handler, array $data = []): TokenContract
1313
{
14+
if ($token && ! $this->isValidTokenName($token)) {
15+
throw new \InvalidArgumentException("Invalid token name [{$token}].");
16+
}
17+
1418
return app()->makeWith(TokenContract::class, compact('token', 'handler', 'data'));
1519
}
1620

1721
public function find(string $token): ?TokenContract
1822
{
23+
if (! $this->isValidTokenName($token)) {
24+
return null;
25+
}
26+
1927
$path = storage_path('statamic/tokens/'.$token.'.yaml');
2028

2129
if (! File::exists($path)) {
@@ -55,6 +63,11 @@ public function collectGarbage(): void
5563
->each->delete();
5664
}
5765

66+
private function isValidTokenName(string $token): bool
67+
{
68+
return (bool) preg_match('/^[A-Za-z0-9_-]+\z/', $token);
69+
}
70+
5871
private function makeFromPath(string $path): FileToken
5972
{
6073
$yaml = YAML::file($path)->parse();

tests/Tokens/TokenRepositoryTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Facades\Statamic\Tokens\Generator;
66
use Illuminate\Support\Carbon;
77
use Illuminate\Support\Collection;
8+
use PHPUnit\Framework\Attributes\DataProvider;
89
use PHPUnit\Framework\Attributes\Test;
910
use Statamic\Contracts\Tokens\Token;
1011
use Statamic\Facades\File;
@@ -129,6 +130,37 @@ public function attempting_to_find_a_non_existent_token_returns_null()
129130
$this->assertNull($this->tokens->find('missing-token'));
130131
}
131132

133+
#[Test]
134+
public function it_prevents_path_traversal_in_find()
135+
{
136+
File::put(storage_path('statamic/evil.yaml'), "handler: 'Handler'\nexpires_at: 9999999999\ndata: []");
137+
138+
$this->assertNull($this->tokens->find('../evil'));
139+
}
140+
141+
#[Test]
142+
#[DataProvider('invalidTokenNameProvider')]
143+
public function it_throws_when_making_a_token_with_an_invalid_name($token)
144+
{
145+
$this->expectException(\InvalidArgumentException::class);
146+
147+
$this->tokens->make($token, 'Handler');
148+
}
149+
150+
public static function invalidTokenNameProvider()
151+
{
152+
return [
153+
'parent traversal' => ['../evil'],
154+
'backslash traversal' => ['..\\evil'],
155+
'nested traversal' => ['foo/../../evil'],
156+
'forward slash' => ['foo/evil'],
157+
'dots only' => ['..'],
158+
'absolute path' => ['/etc/passwd'],
159+
'windows drive' => ['C:\\evil'],
160+
'trailing newline' => ["evil\n"],
161+
];
162+
}
163+
132164
#[Test]
133165
public function it_deletes_expired_tokens()
134166
{

0 commit comments

Comments
 (0)