diff --git a/.gitignore b/.gitignore index f470ba75..c7cf933c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ coverage/ clover.xml tests/Etc/tmp/queuetest.json docker-compose.override.yml +.DS_Store diff --git a/src/Controllers/TenantAssetsController.php b/src/Controllers/TenantAssetsController.php index 5549da0d..1e2014a7 100644 --- a/src/Controllers/TenantAssetsController.php +++ b/src/Controllers/TenantAssetsController.php @@ -18,7 +18,7 @@ class TenantAssetsController extends Controller public function asset($path = null) { - abort_if($path === null, 404); + $this->validatePath($path); try { return response()->file(storage_path("app/public/$path")); @@ -26,4 +26,20 @@ class TenantAssetsController extends Controller abort(404); } } + + /** + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + */ + protected function validatePath(string|null $path): void + { + abort_if($path === null, 404); + + $allowedRoot = storage_path('app/public'); + + // Prevent path traversal attacks. This is generally a non-issue on modern + // webservers but it's still worth handling on the application level as well. + if (! str(realpath("{$allowedRoot}/{$path}"))->startsWith($allowedRoot)) { + abort(403); + } + } } diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php index 703ac65e..c24767cc 100644 --- a/tests/TenantAssetTest.php +++ b/tests/TenantAssetTest.php @@ -141,4 +141,17 @@ class TenantAssetTest extends TestCase $response->assertNotFound(); } + public function test_asset_controller_returns_a_403_when_an_invalid_path_is_provided() + { + TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class; + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + $response = $this->get(tenant_asset('../foo.txt'), [ + 'X-Tenant' => $tenant->id, + ]); + + $response->assertForbidden(); + } }