Skip to content

Commit

Permalink
[5.x] Prevent asset folder path traversal (#11136)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonvarga authored Nov 18, 2024
1 parent 4a9fc2e commit 4cc2c9b
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 2 deletions.
11 changes: 10 additions & 1 deletion src/Assets/AssetFolder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down
8 changes: 7 additions & 1 deletion src/Http/Controllers/CP/Assets/FolderActionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()]);
}
});
}
}
101 changes: 101 additions & 0 deletions tests/Actions/DeleteAssetFolderTest.php
Original file line number Diff line number Diff line change
@@ -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');
}
}
10 changes: 10 additions & 0 deletions tests/Assets/AssetFolderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
{
Expand Down

0 comments on commit 4cc2c9b

Please sign in to comment.