Verified Commit fd361543 authored by Dave Long's avatar Dave Long
Browse files

Issue #3298701 by cmlara, smustgrave, Rishabh Vishwakarma, Tanuj., larowlan,...

Issue #3298701 by cmlara, smustgrave, Rishabh Vishwakarma, Tanuj., larowlan, benjifisher, BramDriesen, catch, Driskell, quietone, effulgentsia: ImageStyleDownloadController routes do not limit schemes served
parent b102e5e9
Loading
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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'

+7 −1
Original line number Diff line number Diff line
@@ -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.
@@ -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);
@@ -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');
    }
+1 −0
Original line number Diff line number Diff line
@@ -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',
+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);
  }

}