diff --git a/src/Http/Controllers/GlideController.php b/src/Http/Controllers/GlideController.php index 655fd4fa2fb..0e9fbcf9da4 100644 --- a/src/Http/Controllers/GlideController.php +++ b/src/Http/Controllers/GlideController.php @@ -16,6 +16,7 @@ use Statamic\Facades\Config; use Statamic\Facades\Glide; use Statamic\Facades\Site; +use Statamic\Facades\URL; use Statamic\Imaging\ImageGenerator; use Statamic\Support\Str; @@ -218,7 +219,12 @@ private function validateSignature() return; } - $path = Str::after($this->request->url(), Site::current()->absoluteUrl()); + $url = $this->request->url(); + $siteUrl = Site::current()->absoluteUrl(); + + $path = Str::startsWith(Str::ensureRight($url, '/'), Str::ensureRight($siteUrl, '/')) + ? Str::after($url, $siteUrl) + : $this->removeSitePrefix($this->request->getPathInfo()); try { SignatureFactory::create(Config::getAppKey())->validateRequest($path, $this->request->query->all()); @@ -226,4 +232,18 @@ private function validateSignature() abort(400, $e->getMessage()); } } + + /** + * Remove the longest site path prefix (e.g. /fr) from the path. + */ + private function removeSitePrefix(string $path): string + { + $prefix = Site::all() + ->map(fn ($site) => Str::removeRight(URL::makeRelative($site->url()), '/')) + ->filter(fn ($prefix) => $prefix !== '' && Str::startsWith($path, $prefix.'/')) + ->sortByDesc(fn ($prefix) => strlen($prefix)) + ->first(); + + return $prefix ? Str::after($path, $prefix) : $path; + } } diff --git a/tests/Imaging/GlideTest.php b/tests/Imaging/GlideTest.php index 4848b8816dc..018ad1d69ba 100644 --- a/tests/Imaging/GlideTest.php +++ b/tests/Imaging/GlideTest.php @@ -397,6 +397,53 @@ public function hybrid_caching_only_registers_the_cache_path_route() $this->get($signedUrl)->assertNotFound(); } + #[Test] + #[DataProvider('signedUrlSiteProvider')] + public function it_validates_the_signature_against_the_site_url($siteUrl) + { + $this->setSites([ + 'en' => ['url' => $siteUrl, 'locale' => 'en_US'], + ]); + + $url = $this->app->make(UrlBuilder::class)->build($this->createAsset(), ['w' => 100]); + + $this->get($url)->assertOk()->streamedContent(); + } + + public static function signedUrlSiteProvider() + { + return [ + 'same domain' => ['http://localhost/'], + 'different domain' => ['https://www.example.com/'], + 'domain starting with the app domain' => ['http://local/'], + ]; + } + + #[Test] + #[DefineEnvironment('pathPrefixedSitesOnAnotherDomain')] + public function it_validates_the_signature_for_a_path_prefixed_site_on_a_different_domain() + { + Site::setCurrent('fr'); + + $url = $this->app->make(UrlBuilder::class)->build($this->createAsset(), ['w' => 100]); + + $this->assertStringStartsWith('/fr/img/', $url); + + $this->get($url)->assertOk()->streamedContent(); + } + + #[Test] + public function it_rejects_an_invalid_signature_when_the_site_url_is_on_a_different_domain() + { + $this->setSites([ + 'en' => ['url' => 'https://www.example.com/', 'locale' => 'en_US'], + ]); + + $url = $this->app->make(UrlBuilder::class)->build($this->createAsset(), ['w' => 100]); + + $this->get(str_replace('w=100', 'w=200', $url))->assertStatus(400); + } + #[Test] public function cache_true_without_cache_path_will_throw_exception() { @@ -619,6 +666,14 @@ private function fakeFfmpeg() }); } + protected function pathPrefixedSitesOnAnotherDomain($app) + { + $app->booting(fn () => $this->setSites([ + 'en' => ['url' => 'https://www.example.com/', 'locale' => 'en_US'], + 'fr' => ['url' => 'https://www.example.com/fr/', 'locale' => 'fr_FR'], + ])); + } + protected function hybridCaching($app) { $app['config']->set('statamic.assets.image_manipulation.cache', 'hybrid');