Loading core/modules/image/image.routing.yml +1 −0 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ image.style_private: path: '/system/files/styles/{image_style}/{scheme}' defaults: _controller: '\Drupal\image\Controller\ImageStyleDownloadController::deliver' required_derivative_scheme: 'private' requirements: _access: 'TRUE' Loading core/modules/image/src/Controller/ImageStyleDownloadController.php +7 −1 Original line number Diff line number Diff line Loading @@ -95,6 +95,8 @@ public static function create(ContainerInterface $container) { * The file scheme, defaults to 'private'. * @param \Drupal\image\ImageStyleInterface $image_style * The image style to deliver. * @param string $required_derivative_scheme * The required scheme for the derivative image. * * @return \Symfony\Component\HttpFoundation\BinaryFileResponse|\Symfony\Component\HttpFoundation\Response * The transferred file as response or some error response. Loading @@ -106,7 +108,7 @@ public static function create(ContainerInterface $container) { * @throws \Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException * Thrown when the file is still being generated. */ public function deliver(Request $request, $scheme, ImageStyleInterface $image_style) { public function deliver(Request $request, $scheme, ImageStyleInterface $image_style, string $required_derivative_scheme) { $target = $request->query->get('file'); $image_uri = $scheme . '://' . $target; $image_uri = $this->streamWrapperManager->normalizeUri($image_uri); Loading Loading @@ -154,6 +156,10 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st $derivative_uri = $image_style->buildUri($image_uri); $derivative_scheme = $this->streamWrapperManager->getScheme($derivative_uri); if ($required_derivative_scheme !== $derivative_scheme) { throw new AccessDeniedHttpException("The scheme for this image doesn't match the scheme for the original image"); } if ($token_is_valid) { $is_public = ($scheme !== 'private'); } Loading core/modules/image/src/Routing/ImageStyleRoutes.php +1 −0 Original line number Diff line number Diff line Loading @@ -56,6 +56,7 @@ public function routes() { '/' . $directory_path . '/styles/{image_style}/{scheme}', [ '_controller' => 'Drupal\image\Controller\ImageStyleDownloadController::deliver', 'required_derivative_scheme' => 'public', ], [ '_access' => 'TRUE', Loading core/modules/image/tests/src/Functional/ImageStyleDownloadAccessControlTest.php 0 → 100644 +133 −0 Original line number Diff line number Diff line <?php namespace Drupal\Tests\image\Functional; use Drupal\Core\StreamWrapper\PublicStream; use Drupal\Core\Url; use Drupal\file\Entity\File; use Drupal\image\Entity\ImageStyle; use Drupal\Tests\BrowserTestBase; /** * Tests access control for downloading image styles. * * @group image */ class ImageStyleDownloadAccessControlTest extends BrowserTestBase { /** * Modules to enable. * * @var array */ protected static $modules = ['image']; /** * {@inheritdoc} */ protected $defaultTheme = 'stark'; /** * The file system. * * @var \Drupal\Core\File\FileSystemInterface */ protected $fileSystem; /** * The image style. * * @var \Drupal\image\ImageStyleInterface */ protected $style; /** * {@inheritdoc} */ protected function prepareEnvironment(): void { parent::prepareEnvironment(); // @see static::testPrivateSchemeWithinPublic() $this->privateFilesDirectory = $this->publicFilesDirectory . '/private'; } /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); $this->fileSystem = $this->container->get('file_system'); $this->style = ImageStyle::create([ 'name' => 'style_foo', 'label' => $this->randomString(), ]); $this->style->save(); // Access control must be respected even when this setting is TRUE. $this->config('image.settings') ->set('allow_insecure_derivatives', TRUE) ->save(); } /** * Ensures that private:// access is forbidden through image.style_public. */ public function testPrivateThroughPublicRoute() { $this->fileSystem->copy(\Drupal::root() . '/core/tests/fixtures/files/image-1.png', 'private://image.png'); // Manually create the file record for the private:// file as we want it // to be temporary to pass hook_download() acl's. $values = [ 'uid' => $this->rootUser->id(), 'status' => 0, 'filename' => 'image.png', 'uri' => 'private://image.png', 'filesize' => filesize('private://image.png'), 'filemime' => 'image/png', ]; $private_file = File::create($values); $private_file->save(); $this->assertNotFalse(getimagesize($private_file->getFileUri())); $token = $this->style->getPathToken('private://image.png'); $public_route_private_scheme = Url::fromRoute( 'image.style_public', [ 'image_style' => $this->style->id(), 'scheme' => 'private', ], ) ->setAbsolute(TRUE); $generate_url = $public_route_private_scheme->toString() . '/image.png?itok=' . $token; $this->drupalLogin($this->rootUser); $this->drupalGet($generate_url); $this->drupalGet(PublicStream::basePath() . '/styles/' . $this->style->id() . '/private/image.png'); $this->assertSession()->statusCodeEquals(403); } /** * Ensures that public:// access is forbidden through image.style.private. */ public function testPublicThroughPrivateRoute() { $this->fileSystem->copy(\Drupal::root() . '/core/tests/fixtures/files/image-1.png', 'public://image.png'); $token = $this->style->getPathToken('public://image.png'); $private_route_public_scheme = Url::fromRoute( 'image.style_private', [ 'image_style' => $this->style->id(), 'scheme' => 'public', ], ) ->setAbsolute(TRUE); $generate_url = $private_route_public_scheme->toString() . '/image.png?itok=' . $token; $this->drupalGet($generate_url); $this->assertSession()->statusCodeEquals(403); } } Loading
core/modules/image/image.routing.yml +1 −0 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ image.style_private: path: '/system/files/styles/{image_style}/{scheme}' defaults: _controller: '\Drupal\image\Controller\ImageStyleDownloadController::deliver' required_derivative_scheme: 'private' requirements: _access: 'TRUE' Loading
core/modules/image/src/Controller/ImageStyleDownloadController.php +7 −1 Original line number Diff line number Diff line Loading @@ -95,6 +95,8 @@ public static function create(ContainerInterface $container) { * The file scheme, defaults to 'private'. * @param \Drupal\image\ImageStyleInterface $image_style * The image style to deliver. * @param string $required_derivative_scheme * The required scheme for the derivative image. * * @return \Symfony\Component\HttpFoundation\BinaryFileResponse|\Symfony\Component\HttpFoundation\Response * The transferred file as response or some error response. Loading @@ -106,7 +108,7 @@ public static function create(ContainerInterface $container) { * @throws \Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException * Thrown when the file is still being generated. */ public function deliver(Request $request, $scheme, ImageStyleInterface $image_style) { public function deliver(Request $request, $scheme, ImageStyleInterface $image_style, string $required_derivative_scheme) { $target = $request->query->get('file'); $image_uri = $scheme . '://' . $target; $image_uri = $this->streamWrapperManager->normalizeUri($image_uri); Loading Loading @@ -154,6 +156,10 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st $derivative_uri = $image_style->buildUri($image_uri); $derivative_scheme = $this->streamWrapperManager->getScheme($derivative_uri); if ($required_derivative_scheme !== $derivative_scheme) { throw new AccessDeniedHttpException("The scheme for this image doesn't match the scheme for the original image"); } if ($token_is_valid) { $is_public = ($scheme !== 'private'); } Loading
core/modules/image/src/Routing/ImageStyleRoutes.php +1 −0 Original line number Diff line number Diff line Loading @@ -56,6 +56,7 @@ public function routes() { '/' . $directory_path . '/styles/{image_style}/{scheme}', [ '_controller' => 'Drupal\image\Controller\ImageStyleDownloadController::deliver', 'required_derivative_scheme' => 'public', ], [ '_access' => 'TRUE', Loading
core/modules/image/tests/src/Functional/ImageStyleDownloadAccessControlTest.php 0 → 100644 +133 −0 Original line number Diff line number Diff line <?php namespace Drupal\Tests\image\Functional; use Drupal\Core\StreamWrapper\PublicStream; use Drupal\Core\Url; use Drupal\file\Entity\File; use Drupal\image\Entity\ImageStyle; use Drupal\Tests\BrowserTestBase; /** * Tests access control for downloading image styles. * * @group image */ class ImageStyleDownloadAccessControlTest extends BrowserTestBase { /** * Modules to enable. * * @var array */ protected static $modules = ['image']; /** * {@inheritdoc} */ protected $defaultTheme = 'stark'; /** * The file system. * * @var \Drupal\Core\File\FileSystemInterface */ protected $fileSystem; /** * The image style. * * @var \Drupal\image\ImageStyleInterface */ protected $style; /** * {@inheritdoc} */ protected function prepareEnvironment(): void { parent::prepareEnvironment(); // @see static::testPrivateSchemeWithinPublic() $this->privateFilesDirectory = $this->publicFilesDirectory . '/private'; } /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); $this->fileSystem = $this->container->get('file_system'); $this->style = ImageStyle::create([ 'name' => 'style_foo', 'label' => $this->randomString(), ]); $this->style->save(); // Access control must be respected even when this setting is TRUE. $this->config('image.settings') ->set('allow_insecure_derivatives', TRUE) ->save(); } /** * Ensures that private:// access is forbidden through image.style_public. */ public function testPrivateThroughPublicRoute() { $this->fileSystem->copy(\Drupal::root() . '/core/tests/fixtures/files/image-1.png', 'private://image.png'); // Manually create the file record for the private:// file as we want it // to be temporary to pass hook_download() acl's. $values = [ 'uid' => $this->rootUser->id(), 'status' => 0, 'filename' => 'image.png', 'uri' => 'private://image.png', 'filesize' => filesize('private://image.png'), 'filemime' => 'image/png', ]; $private_file = File::create($values); $private_file->save(); $this->assertNotFalse(getimagesize($private_file->getFileUri())); $token = $this->style->getPathToken('private://image.png'); $public_route_private_scheme = Url::fromRoute( 'image.style_public', [ 'image_style' => $this->style->id(), 'scheme' => 'private', ], ) ->setAbsolute(TRUE); $generate_url = $public_route_private_scheme->toString() . '/image.png?itok=' . $token; $this->drupalLogin($this->rootUser); $this->drupalGet($generate_url); $this->drupalGet(PublicStream::basePath() . '/styles/' . $this->style->id() . '/private/image.png'); $this->assertSession()->statusCodeEquals(403); } /** * Ensures that public:// access is forbidden through image.style.private. */ public function testPublicThroughPrivateRoute() { $this->fileSystem->copy(\Drupal::root() . '/core/tests/fixtures/files/image-1.png', 'public://image.png'); $token = $this->style->getPathToken('public://image.png'); $private_route_public_scheme = Url::fromRoute( 'image.style_private', [ 'image_style' => $this->style->id(), 'scheme' => 'public', ], ) ->setAbsolute(TRUE); $generate_url = $private_route_public_scheme->toString() . '/image.png?itok=' . $token; $this->drupalGet($generate_url); $this->assertSession()->statusCodeEquals(403); } }