diff --git a/core/modules/image/config/image.settings.yml b/core/modules/image/config/image.settings.yml index c6dadc8..f0f123f 100644 --- a/core/modules/image/config/image.settings.yml +++ b/core/modules/image/config/image.settings.yml @@ -1 +1,2 @@ preview_image: core/modules/image/sample.png +allow_insecure_derivatives: false diff --git a/core/modules/image/image.module b/core/modules/image/image.module index d7b02d0..b2816de 100644 --- a/core/modules/image/image.module +++ b/core/modules/image/image.module @@ -37,6 +37,11 @@ */ define('IMAGE_STORAGE_MODULE', IMAGE_STORAGE_OVERRIDE | IMAGE_STORAGE_DEFAULT); +/** + * The name of the query parameter for image derivative tokens. + */ +define('IMAGE_DERIVATIVE_TOKEN', 'itok'); + // Load all Field module hooks for Image. require_once DRUPAL_ROOT . '/core/modules/image/image.field.inc'; @@ -587,16 +592,25 @@ function image_style_options($include_empty = TRUE) { * The image style */ function image_style_deliver($style, $scheme) { - // Check that the style is defined and the scheme is valid. - if (!$style || !file_stream_wrapper_valid_scheme($scheme)) { - drupal_exit(); - } - $args = func_get_args(); array_shift($args); array_shift($args); $target = implode('/', $args); + // Check that the style is defined, the scheme is valid, and the image + // derivative token is valid. (Sites which require image derivatives to be + // generated without a token can set the + // 'image.settings:allow_insecure_derivatives' configuration to TRUE to bypass + // the latter check, but this will increase the site's vulnerability to + // denial-of-service attacks.) + $valid = !empty($style) && file_stream_wrapper_valid_scheme($scheme); + if (!config('image.settings')->get('allow_insecure_derivatives')) { + $valid = $valid && isset($_GET[IMAGE_DERIVATIVE_TOKEN]) && $_GET[IMAGE_DERIVATIVE_TOKEN] === image_style_path_token($style->name, $scheme . '://' . $target); + } + if (!$valid) { + throw new AccessDeniedHttpException(); + } + $image_uri = $scheme . '://' . $target; $derivative_uri = image_style_path($style->id(), $image_uri); @@ -784,6 +798,10 @@ function image_style_flush($style) { */ function image_style_url($style_name, $path) { $uri = image_style_path($style_name, $path); + // The token query is added even if the + // 'image.settings:allow_insecure_derivatives' configuration is TRUE, so that + // the emitted links remain valid if it is changed back to the default FALSE. + $token_query = array(IMAGE_DERIVATIVE_TOKEN => image_style_path_token($style_name, file_stream_wrapper_uri_normalize($path))); // If not using clean URLs, the image derivative callback is only available // with the script path. If the file does not exist, use url() to ensure @@ -791,10 +809,33 @@ function image_style_url($style_name, $path) { // actual file path, this avoids bootstrapping PHP once the files are built. if ($GLOBALS['script_path'] && file_uri_scheme($uri) == 'public' && !file_exists($uri)) { $directory_path = file_stream_wrapper_get_instance_by_uri($uri)->getDirectoryPath(); - return url($directory_path . '/' . file_uri_target($uri), array('absolute' => TRUE)); + return url($directory_path . '/' . file_uri_target($uri), array('absolute' => TRUE, 'query' => $token_query)); } - return file_create_url($uri); + $file_url = file_create_url($uri); + // Append the query string with the token. + return $file_url . (strpos($file_url, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($token_query); +} + +/** + * Generates a token to protect an image style derivative. + * + * This prevents unauthorized generation of an image style derivative, + * which can be costly both in CPU time and disk space. + * + * @param string $style_name + * The name of the image style. + * @param string $uri + * The URI of the image for this style, for example as returned by + * image_style_path(). + * + * @return string + * An eight-character token which can be used to protect image style + * derivatives against denial-of-service attacks. + */ +function image_style_path_token($style_name, $uri) { + // Return the first eight characters. + return substr(drupal_hmac_base64($style_name . ':' . $uri, drupal_get_private_key() . drupal_get_hash_salt()), 0, 8); } /** diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageStylesPathAndUrlTest.php b/core/modules/image/lib/Drupal/image/Tests/ImageStylesPathAndUrlTest.php index cbf49e2..f755ba0 100644 --- a/core/modules/image/lib/Drupal/image/Tests/ImageStylesPathAndUrlTest.php +++ b/core/modules/image/lib/Drupal/image/Tests/ImageStylesPathAndUrlTest.php @@ -84,9 +84,16 @@ function testImageStyleUrlAndPathPrivateUnclean() { } /** - * Test image_style_url(). + * Tests image_style_url() with a file URL that has an extra slash in it. */ - function _testImageStyleUrlAndPath($scheme, $clean_url = TRUE) { + function testImageStyleUrlExtraSlash() { + $this->_testImageStyleUrlAndPath('public', TRUE, TRUE); + } + + /** + * Tests image_style_url(). + */ + function _testImageStyleUrlAndPath($scheme, $clean_url = TRUE, $extra_slash = FALSE) { $script_path_original = $GLOBALS['script_path']; $GLOBALS['script_path'] = $clean_url ? '' : 'index.php/'; @@ -110,13 +117,28 @@ function _testImageStyleUrlAndPath($scheme, $clean_url = TRUE) { $this->assertNotIdentical(FALSE, $original_uri, 'Created the generated image file.'); // Get the URL of a file that has not been generated and try to create it. - $generated_uri = $scheme . '://styles/' . $this->style_name . '/' . $scheme . '/'. drupal_basename($original_uri); + $generated_uri = image_style_path($this->style_name, $original_uri); $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); $generate_url = image_style_url($this->style_name, $original_uri); + // Ensure that the tests still pass when the file is generated by accessing + // a poorly constructed (but still valid) file URL that has an extra slash + // in it. + if ($extra_slash) { + $modified_uri = str_replace('://', ':///', $original_uri); + $this->assertNotEqual($original_uri, $modified_uri, 'An extra slash was added to the generated file URI.'); + $generate_url = image_style_url($this->style_name, $modified_uri); + } + if ($GLOBALS['script_path']) { $this->assertTrue(strpos($generate_url, $GLOBALS['script_path']) !== FALSE, 'When using non-clean URLS, the system path contains the script name.'); } + // Add some extra chars to the token. + $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', IMAGE_DERIVATIVE_TOKEN . '=Zo', $generate_url)); + $this->assertResponse(403, 'Image was inaccessible at the URL wih an invalid token.'); + // Change the parameter name so the token is missing. + $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', 'wrongparam=', $generate_url)); + $this->assertResponse(403, 'Image was inaccessible at the URL wih a missing token.'); // Fetch the URL that generates the file. $this->drupalGet($generate_url); @@ -162,6 +184,11 @@ function _testImageStyleUrlAndPath($scheme, $clean_url = TRUE) { $this->assertNoRaw( chr(137) . chr(80) . chr(78) . chr(71) . chr(13) . chr(10) . chr(26) . chr(10), 'No PNG signature found in the response body.'); } } + elseif (!$GLOBALS['script_path']) { + // Add some extra chars to the token. + $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', IMAGE_DERIVATIVE_TOKEN . '=Zo', $generate_url)); + $this->assertResponse(200, 'Existing image was accessible at the URL wih an invalid token.'); + } $GLOBALS['script_path'] = $script_path_original; }