diff --git a/src/Tokens/FileTokenRepository.php b/src/Tokens/FileTokenRepository.php index 044112fba8f..7b19622d931 100644 --- a/src/Tokens/FileTokenRepository.php +++ b/src/Tokens/FileTokenRepository.php @@ -11,11 +11,19 @@ class FileTokenRepository extends TokenRepository { public function make(?string $token, string $handler, array $data = []): TokenContract { + if ($token && ! $this->isValidTokenName($token)) { + throw new \InvalidArgumentException("Invalid token name [{$token}]."); + } + return app()->makeWith(TokenContract::class, compact('token', 'handler', 'data')); } public function find(string $token): ?TokenContract { + if (! $this->isValidTokenName($token)) { + return null; + } + $path = storage_path('statamic/tokens/'.$token.'.yaml'); if (! File::exists($path)) { @@ -55,6 +63,11 @@ public function collectGarbage(): void ->each->delete(); } + private function isValidTokenName(string $token): bool + { + return (bool) preg_match('/^[A-Za-z0-9_-]+\z/', $token); + } + private function makeFromPath(string $path): FileToken { $yaml = YAML::file($path)->parse(); diff --git a/tests/Tokens/TokenRepositoryTest.php b/tests/Tokens/TokenRepositoryTest.php index 7ad1172bae5..e26245b8346 100644 --- a/tests/Tokens/TokenRepositoryTest.php +++ b/tests/Tokens/TokenRepositoryTest.php @@ -5,6 +5,7 @@ use Facades\Statamic\Tokens\Generator; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Statamic\Contracts\Tokens\Token; use Statamic\Facades\File; @@ -129,6 +130,37 @@ public function attempting_to_find_a_non_existent_token_returns_null() $this->assertNull($this->tokens->find('missing-token')); } + #[Test] + public function it_prevents_path_traversal_in_find() + { + File::put(storage_path('statamic/evil.yaml'), "handler: 'Handler'\nexpires_at: 9999999999\ndata: []"); + + $this->assertNull($this->tokens->find('../evil')); + } + + #[Test] + #[DataProvider('invalidTokenNameProvider')] + public function it_throws_when_making_a_token_with_an_invalid_name($token) + { + $this->expectException(\InvalidArgumentException::class); + + $this->tokens->make($token, 'Handler'); + } + + public static function invalidTokenNameProvider() + { + return [ + 'parent traversal' => ['../evil'], + 'backslash traversal' => ['..\\evil'], + 'nested traversal' => ['foo/../../evil'], + 'forward slash' => ['foo/evil'], + 'dots only' => ['..'], + 'absolute path' => ['/etc/passwd'], + 'windows drive' => ['C:\\evil'], + 'trailing newline' => ["evil\n"], + ]; + } + #[Test] public function it_deletes_expired_tokens() {