CVE-2024-52600
Description
Statmatic is a Laravel and Git powered content management system (CMS). Prior to version 5.17.0, assets uploaded with appropriately crafted filenames may result in them being placed in a location different than what was configured. The issue affects front-end forms with assets fields and other places where assets can be uploaded, although users would need upload permissions anyway. Files can be uploaded so they would be located on the server in a different location, and potentially override existing files. Traversal outside an asset container is not possible. This path traversal vulnerability has been fixed in 5.17.0.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Path traversal in Statmatic CMS before 5.17.0 allows crafted filenames to upload assets to unintended locations, potentially overwriting existing files.
Vulnerability
Overview
CVE-2024-52600 is a path traversal vulnerability in Statmatic, a Laravel and Git powered content management system. Affecting versions prior to 5.17.0, the flaw allows assets uploaded with specially crafted filenames to be placed in a location different from what was configured. The vulnerability exists in front-end forms with assets fields and other asset upload interfaces, as well as potentially other upload locations [1].
Exploitation
Mechanism
An authenticated user with upload permissions can exploit this by uploading files with path traversal sequences (e.g., ../ or URL-encoded variations like %2e%2e%2f) in the filename. The references show that tests were added to verify that such filenames are either sanitized (by replacing path traversal components with dashes) or explicitly rejected. For example, a filename like ../test.jpg would be stored as path/to/..-test.jpg after sanitization, and direct path traversal in the path() setter method throws a PathTraversalDetected exception [2][3]. Prior to the fix, these checks were absent, allowing the upload to write to a directory outside the intended container, although traversal outside the configured asset container is not possible [1].
Impact
Successful exploitation could allow an attacker to upload files to unintended directories on the server, potentially overwriting existing files. While the attacker cannot escape the configured asset container entirely, the ability to place files in arbitrary locations within that container could lead to overwriting critical assets or misdirecting uploaded content [1].
Mitigation
The vulnerability has been fixed in Statmatic version 5.17.0. The fixes include adding path traversal detection in the Asset::path() setter and proper sanitization in the upload handling logic, as evidenced by the commits referenced in the advisory [2][3][4]. Users should upgrade to the latest version to protect against this vulnerability.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
statamic/cmsPackagist | < 5.17.0 | 5.17.0 |
Affected products
3Patches
3400875b20f40[5.x] More path traversal fixes (#11140)
4 files changed · +53 −4
src/Assets/Asset.php+8 −0 modified@@ -7,6 +7,7 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Cache; +use League\Flysystem\PathTraversalDetected; use Statamic\Assets\AssetUploader as Uploader; use Statamic\Contracts\Assets\Asset as AssetContract; use Statamic\Contracts\Assets\AssetContainer as AssetContainerContract; @@ -367,6 +368,13 @@ public function path($path = null) { return $this ->fluentlyGetOrSet('path') + ->setter(function ($path) { + if (str_contains($path, '../')) { + throw PathTraversalDetected::forPath($path); + } + + return $path; + }) ->getter(function ($path) { return $path ? ltrim($path, '/') : null; })
tests/Assets/AssetContainerTest.php+11 −0 modified@@ -14,6 +14,7 @@ use League\Flysystem\DirectoryAttributes; use League\Flysystem\DirectoryListing; use League\Flysystem\FileAttributes; +use League\Flysystem\PathTraversalDetected; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Statamic\Assets\Asset; @@ -811,6 +812,16 @@ public function it_makes_an_asset_at_given_path() $this->assertEquals('path/to/test.txt', $asset->path()); } + #[Test] + public function it_cannot_make_an_asset_using_path_traversal() + { + $this->expectException(PathTraversalDetected::class); + $this->expectExceptionMessage('Path traversal detected: foo/../test.txt'); + + $container = $this->containerWithDisk(); + $container->makeAsset('foo/../test.txt'); + } + #[Test] public function it_gets_all_assets_by_default() {
tests/Assets/AssetFolderTest.php+14 −4 modified@@ -57,12 +57,22 @@ public function it_gets_and_sets_the_path() } #[Test] - public function path_traversal_not_allowed() + public function it_cannot_use_traversal_in_path() { - $this->expectException(PathTraversalDetected::class); - $this->expectExceptionMessage('Path traversal detected: path/to/../folder'); + $folder = (new Folder)->path('path/to/folder'); - (new Folder)->path('path/to/../folder'); + try { + $folder->path('path/to/../folder'); + } catch (PathTraversalDetected $e) { + $this->assertEquals('Path traversal detected: path/to/../folder', $e->getMessage()); + + // Even if exception was thrown, make sure that the path didn't somehow get updated. + $this->assertEquals('path/to/folder', $folder->path()); + + return; + } + + $this->fail('Exception was not thrown.'); } #[Test]
tests/Assets/AssetTest.php+20 −0 modified@@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Storage; +use League\Flysystem\PathTraversalDetected; use Mockery; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; @@ -482,6 +483,25 @@ public function it_gets_and_sets_the_path() $this->assertEquals('asset.jpg', $asset->path('asset.jpg')->path()); } + #[Test] + public function it_cannot_use_traversal_in_path() + { + $asset = (new Asset)->path('path/to/asset.jpg'); + + try { + $asset->path('foo/../test.jpg'); + } catch (PathTraversalDetected $e) { + $this->assertEquals('Path traversal detected: foo/../test.jpg', $e->getMessage()); + + // Even if exception was thrown, make sure that the path didn't somehow get updated. + $this->assertEquals('path/to/asset.jpg', $asset->path()); + + return; + } + + $this->fail('Exception was not thrown.'); + } + #[Test] public function it_gets_the_id_from_the_container_and_path() {
0c07c10009a2[5.x] Add upload path traversal tests (#11139)
1 file changed · +44 −15
tests/Feature/Assets/StoreAssetTest.php+44 −15 modified@@ -5,6 +5,7 @@ use Illuminate\Http\UploadedFile; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Storage; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Statamic\Assets\AssetContainer; use Statamic\Facades; @@ -37,22 +38,35 @@ public function setUp(): void } #[Test] - public function it_uploads_an_asset() + #[DataProvider('uploadProvider')] + public function it_uploads_an_asset($filename, $expected) { - Storage::disk('test')->assertMissing('path/to/test.jpg'); + Storage::disk('test')->assertMissing($expected); $this ->actingAs($this->userWithPermission()) - ->submit() - ->assertOk() + ->submit([ + 'file' => UploadedFile::fake()->image($filename), + ]) ->assertJson([ 'data' => [ - 'id' => 'test_container::path/to/test.jpg', - 'path' => 'path/to/test.jpg', + 'id' => 'test_container::'.$expected, + 'path' => $expected, ], ]); - Storage::disk('test')->assertExists('path/to/test.jpg'); + Storage::disk('test')->assertExists($expected); + } + + public static function uploadProvider() + { + return [ + 'test.jpg' => ['test.jpg', 'path/to/test.jpg'], + + // path traversal naughtiness + '../test.jpg urlencoded' => ['%2e%2e%2ftest.jpg', 'path/to/..-test.jpg'], + 'foo/../test.jpg urlencoded' => ['foo%2f%2e%2e%2ftest.jpg', 'path/to/foo-..-test.jpg'], + ]; } #[Test] @@ -187,24 +201,39 @@ public function it_can_upload_with_different_filename() } #[Test] - public function it_can_upload_to_relative_path() + #[DataProvider('relativePathProvider')] + public function it_can_upload_to_relative_path($filename, $expected) { - Storage::disk('test')->assertMissing('path/to/test.jpg'); - Storage::disk('test')->assertMissing('path/to/sub/folder/test.jpg'); + Storage::disk('test')->assertMissing('path/to/'.$filename); + Storage::disk('test')->assertMissing($expected); $this ->actingAs($this->userWithPermission()) - ->submit(['relativePath' => 'sub/folder']) + ->submit([ + 'relativePath' => 'sub/folder', + 'file' => UploadedFile::fake()->image($filename), + ]) ->assertOk() ->assertJson([ 'data' => [ - 'id' => 'test_container::path/to/sub/folder/test.jpg', - 'path' => 'path/to/sub/folder/test.jpg', + 'id' => 'test_container::'.$expected, + 'path' => $expected, ], ]); - Storage::disk('test')->assertMissing('path/to/test.jpg'); - Storage::disk('test')->assertExists('path/to/sub/folder/test.jpg'); + Storage::disk('test')->assertMissing('path/to/'.$filename); + Storage::disk('test')->assertExists($expected); + } + + public static function relativePathProvider() + { + return [ + 'test.jpg' => ['test.jpg', 'path/to/sub/folder/test.jpg'], + + // path traversal naughtiness + '../test.jpg urlencoded' => ['%2e%2e%2ftest.jpg', 'path/to/sub/folder/..-test.jpg'], + 'foo/../test.jpg urlencoded' => ['foo%2f%2e%2e%2ftest.jpg', 'path/to/sub/folder/foo-..-test.jpg'], + ]; } #[Test]
4cc2c9bd0f39[5.x] Prevent asset folder path traversal (#11136)
4 files changed · +128 −2
src/Assets/AssetFolder.php+10 −1 modified@@ -3,6 +3,7 @@ namespace Statamic\Assets; use Illuminate\Contracts\Support\Arrayable; +use League\Flysystem\PathTraversalDetected; use Statamic\Assets\AssetUploader as Uploader; use Statamic\Contracts\Assets\AssetFolder as Contract; use Statamic\Events\AssetFolderDeleted; @@ -35,7 +36,15 @@ public function container($container = null) public function path($path = null) { - return $this->fluentlyGetOrSet('path')->args(func_get_args()); + return $this->fluentlyGetOrSet('path') + ->setter(function ($path) { + if (str_contains($path, '..')) { + throw PathTraversalDetected::forPath($path); + } + + return $path; + }) + ->args(func_get_args()); } public function basename()
src/Http/Controllers/CP/Assets/FolderActionController.php+7 −1 modified@@ -2,7 +2,9 @@ namespace Statamic\Http\Controllers\CP\Assets; +use League\Flysystem\PathTraversalDetected; use Statamic\Assets\AssetFolder; +use Statamic\Exceptions\ValidationException; use Statamic\Http\Controllers\CP\ActionController as Controller; class FolderActionController extends Controller @@ -12,7 +14,11 @@ class FolderActionController extends Controller protected function getSelectedItems($items, $context) { return $items->map(function ($path) use ($context) { - return AssetFolder::find("{$context['container']}::{$path}"); + try { + return AssetFolder::find("{$context['container']}::{$path}"); + } catch (PathTraversalDetected $e) { + throw ValidationException::withMessages(['selections' => $e->getMessage()]); + } }); } }
tests/Actions/DeleteAssetFolderTest.php+101 −0 added@@ -0,0 +1,101 @@ +<?php + +namespace Tests\Actions; + +use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Storage; +use PHPUnit\Framework\Attributes\Test; +use Statamic\Assets\AssetContainer; +use Statamic\Facades\User; +use Tests\FakesRoles; +use Tests\PreventSavingStacheItemsToDisk; +use Tests\TestCase; + +class DeleteAssetFolderTest extends TestCase +{ + use FakesRoles; + use PreventSavingStacheItemsToDisk; + + private $container; + + public function setUp(): void + { + parent::setUp(); + + Storage::fake('test'); + + $this->container = tap( + (new AssetContainer)->handle('test_container')->disk('test') + )->save(); + } + + private function createAsset($filename) + { + $file = UploadedFile::fake()->image($filename, 30, 60); + Storage::disk('test')->putFileAs($file, $filename); + } + + private function assertAssetExists($file) + { + Storage::disk('test')->assertExists($file); + $this->assertNotNull($this->container->asset($file)); + } + + private function assertAssetDoesNotExist($file) + { + Storage::disk('test')->assertMissing($file); + $this->assertNull($this->container->asset($file)); + } + + private function deleteFolder($folder) + { + return $this->post(cp_route('assets.folders.actions.run', ['asset_container' => 'test_container']), [ + 'action' => 'delete', + 'context' => ['container' => 'test_container'], + 'selections' => [$folder], + 'values' => [], + ]); + } + + #[Test] + public function it_deletes() + { + $this->createAsset('foo/alfa.jpg'); + $this->createAsset('foo/bravo.jpg'); + $this->createAsset('bar/charlie.jpg'); + $this->createAsset('delta.jpg'); + Storage::disk('test')->assertExists('foo'); + + $this + ->actingAs(tap(User::make()->makeSuper())->save()) + ->deleteFolder('foo') + ->assertOk(); + + Storage::disk('test')->assertMissing('foo'); + $this->assertAssetDoesNotExist('foo/alfa.jpg'); + $this->assertAssetDoesNotExist('foo/bravo.jpg'); + $this->assertAssetExists('bar/charlie.jpg'); + $this->assertAssetExists('delta.jpg'); + } + + #[Test] + public function no_path_traversal() + { + $this->createAsset('foo/alfa.jpg'); + $this->createAsset('foo/bravo.jpg'); + $this->createAsset('bar/charlie.jpg'); + $this->createAsset('delta.jpg'); + Storage::disk('test')->assertExists('foo'); + + $this + ->actingAs(tap(User::make()->makeSuper())->save()) + ->deleteFolder('foo/..') + ->assertSessionHasErrors(['selections' => 'Path traversal detected: foo/..']); + + Storage::disk('test')->assertExists('foo'); + $this->assertAssetExists('foo/alfa.jpg'); + $this->assertAssetExists('foo/bravo.jpg'); + $this->assertAssetExists('bar/charlie.jpg'); + $this->assertAssetExists('delta.jpg'); + } +}
tests/Assets/AssetFolderTest.php+10 −0 modified@@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; +use League\Flysystem\PathTraversalDetected; use PHPUnit\Framework\Attributes\Test; use Statamic\Assets\Asset; use Statamic\Assets\AssetContainerContents; @@ -55,6 +56,15 @@ public function it_gets_and_sets_the_path() $this->assertEquals('folder', $folder->basename()); } + #[Test] + public function path_traversal_not_allowed() + { + $this->expectException(PathTraversalDetected::class); + $this->expectExceptionMessage('Path traversal detected: path/to/../folder'); + + (new Folder)->path('path/to/../folder'); + } + #[Test] public function it_gets_the_disk_from_the_container() {
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-p7f6-8mcm-fwv3ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-52600ghsaADVISORY
- github.com/statamic/cms/commit/0c07c10009a2439c8ee56c8faefd1319dc6e388dnvdWEB
- github.com/statamic/cms/commit/400875b20f40e1343699d536a432a6fc284346danvdWEB
- github.com/statamic/cms/commit/4cc2c9bd0f39a93b3fc7e9ef0f12792576fd380dnvdWEB
- github.com/statamic/cms/security/advisories/GHSA-p7f6-8mcm-fwv3nvdWEB
News mentions
0No linked articles in our index yet.