Origin: vendor
Forwarded: not-needed
From: Gunnar Wolf <gwolf@debian.org>
Last-update: 2018-02-23
Description: Fixes for SA-CORE-2018-001 (several vulnerabilities)
 Backporting the diff between 7.56 and 7.57, applying it to the
 version in the Stable Debian release (7.52). For further details, the
 advisory is in:
 .
 https://www.drupal.org/SA-CORE-2018-001
 .
 CVE identifiers not yet assigned; descriptors:
 - External link injection on 404 pages when linking to the current
   page
 - jQuery vulnerability with untrusted domains
 - Private file access bypass
 - JavaScript cross-site scripting prevention is incomplete

Index: drupal7/includes/common.inc
===================================================================
--- drupal7.orig/includes/common.inc
+++ drupal7/includes/common.inc
@@ -2189,8 +2189,11 @@ function url($path = NULL, array $option
     'prefix' => ''
   );
 
+  // Determine whether this is an external link, but ensure that the current
+  // path is always treated as internal by default (to prevent external link
+  // injection vulnerabilities).
   if (!isset($options['external'])) {
-    $options['external'] = url_is_external($path);
+    $options['external'] = $path === $_GET['q'] ? FALSE : url_is_external($path);
   }
 
   // Preserve the original path before altering or aliasing.
Index: drupal7/misc/drupal.js
===================================================================
--- drupal7.orig/misc/drupal.js
+++ drupal7/misc/drupal.js
@@ -28,6 +28,42 @@ $.fn.init = function (selector, context,
 $.fn.init.prototype = jquery_init.prototype;
 
 /**
+ * Pre-filter Ajax requests to guard against XSS attacks.
+ *
+ * See https://github.com/jquery/jquery/issues/2432
+ */
+if ($.ajaxPrefilter) {
+  // For newer versions of jQuery, use an Ajax prefilter to prevent
+  // auto-executing script tags from untrusted domains. This is similar to the
+  // fix that is built in to jQuery 3.0 and higher.
+  $.ajaxPrefilter(function (s) {
+    if (s.crossDomain) {
+      s.contents.script = false;
+    }
+  });
+}
+else if ($.httpData) {
+  // For the version of jQuery that ships with Drupal core, override
+  // jQuery.httpData to prevent auto-detecting "script" data types from
+  // untrusted domains.
+  var jquery_httpData = $.httpData;
+  $.httpData = function (xhr, type, s) {
+    // @todo Consider backporting code from newer jQuery versions to check for
+    //   a cross-domain request here, rather than using Drupal.urlIsLocal() to
+    //   block scripts from all URLs that are not on the same site.
+    if (!type && !Drupal.urlIsLocal(s.url)) {
+      var content_type = xhr.getResponseHeader('content-type') || '';
+      if (content_type.indexOf('javascript') >= 0) {
+        // Default to a safe data type.
+        type = 'text';
+      }
+    }
+    return jquery_httpData.call(this, xhr, type, s);
+  };
+  $.httpData.prototype = jquery_httpData.prototype;
+}
+
+/**
  * Attach all registered behaviors to a page element.
  *
  * Behaviors are event-triggered actions that attach to page elements, enhancing
@@ -137,7 +173,7 @@ Drupal.detachBehaviors = function (conte
  */
 Drupal.checkPlain = function (str) {
   var character, regex,
-      replace = { '&': '&amp;', '"': '&quot;', '<': '&lt;', '>': '&gt;' };
+      replace = { '&': '&amp;', "'": '&#39;', '"': '&quot;', '<': '&lt;', '>': '&gt;' };
   str = String(str);
   for (character in replace) {
     if (replace.hasOwnProperty(character)) {
Index: drupal7/modules/file/file.module
===================================================================
--- drupal7.orig/modules/file/file.module
+++ drupal7/modules/file/file.module
@@ -140,7 +140,7 @@ function file_file_download($uri, $field
   }
 
   // Find out which (if any) fields of this type contain the file.
-  $references = file_get_file_references($file, NULL, FIELD_LOAD_CURRENT, $field_type);
+  $references = file_get_file_references($file, NULL, FIELD_LOAD_CURRENT, $field_type, FALSE);
 
   // Stop processing if there are no references in order to avoid returning
   // headers for files controlled by other modules. Make an exception for
@@ -1012,11 +1012,18 @@ function file_icon_map($file) {
  * @param $field_type
  *   (optional) The name of a field type. If given, limits the reference check
  *   to fields of the given type.
+ * @param $check_access
+ *   (optional) A boolean that specifies whether the permissions of the current
+ *   user should be checked when retrieving references. If FALSE, all
+ *   references to the file are returned. If TRUE, only references from
+ *   entities that the current user has access to are returned. Defaults to
+ *   TRUE for backwards compatibility reasons, but FALSE is recommended for
+ *   most situations.
  *
  * @return
  *   An integer value.
  */
-function file_get_file_references($file, $field = NULL, $age = FIELD_LOAD_REVISION, $field_type = 'file') {
+function file_get_file_references($file, $field = NULL, $age = FIELD_LOAD_REVISION, $field_type = 'file', $check_access = TRUE) {
   $references = drupal_static(__FUNCTION__, array());
   $fields = isset($field) ? array($field['field_name'] => $field) : field_info_fields();
 
@@ -1027,6 +1034,11 @@ function file_get_file_references($file,
       $query
         ->fieldCondition($file_field, 'fid', $file->fid)
         ->age($age);
+      if (!$check_access) {
+        // Neutralize the 'entity_field_access' query tag added by
+        // field_sql_storage_field_storage_query().
+        $query->addTag('DANGEROUS_ACCESS_CHECK_OPT_OUT');
+      }
       $references[$field_name] = $query->execute();
     }
   }
Index: drupal7/modules/file/tests/file.test
===================================================================
--- drupal7.orig/modules/file/tests/file.test
+++ drupal7/modules/file/tests/file.test
@@ -1527,4 +1527,77 @@ class FilePrivateTestCase extends FileFi
     $this->drupalGet($file_url);
     $this->assertResponse(403, 'Confirmed that another anonymous user cannot access the permanent file when it is referenced by an unpublished node.');
   }
+
+  /**
+   * Tests file access for private nodes when file download access is granted.
+   */
+  function testPrivateFileDownloadAccessGranted() {
+    // Tell file_module_test to attempt to grant access to all private files,
+    // and ensure that it is doing so correctly.
+    $test_file = $this->getTestFile('text');
+    $uri = file_unmanaged_move($test_file->uri, 'private://');
+    $file_url = file_create_url($uri);
+    $this->drupalGet($file_url);
+    $this->assertResponse(403, 'Access is not granted to an arbitrary private file by default.');
+    variable_set('file_module_test_grant_download_access', TRUE);
+    $this->drupalGet($file_url);
+    $this->assertResponse(200, 'Access is granted to an arbitrary private file after a module grants access to all private files in hook_file_download().');
+
+    // Create a public node with a file attached.
+    $type_name = 'page';
+    $field_name = strtolower($this->randomName());
+    $this->createFileField($field_name, $type_name, array('uri_scheme' => 'private'));
+    $test_file = $this->getTestFile('text');
+    $nid = $this->uploadNodeFile($test_file, $field_name, $type_name, TRUE, array('private' => FALSE));
+    $node = node_load($nid, NULL, TRUE);
+    $file_url = file_create_url($node->{$field_name}[LANGUAGE_NONE][0]['uri']);
+
+    // Unpublish the node and ensure that only administrators (not anonymous
+    // users) can access the node and download the file; the expectation is
+    // that the File module's hook_file_download() implementation will deny
+    // access and thereby override the file_module_test module's access grant.
+    $node->status = NODE_NOT_PUBLISHED;
+    node_save($node);
+    $this->drupalLogin($this->admin_user);
+    $this->drupalGet("node/$nid");
+    $this->assertResponse(200, 'Administrator can access the unpublished node.');
+    $this->drupalGet($file_url);
+    $this->assertResponse(200, 'Administrator can download the file attached to the unpublished node.');
+    $this->drupalLogOut();
+    $this->drupalGet("node/$nid");
+    $this->assertResponse(403, 'Anonymous user cannot access the unpublished node.');
+    $this->drupalGet($file_url);
+    $this->assertResponse(403, 'Anonymous user cannot download the file attached to the unpublished node.');
+
+    // Re-publish the node and ensure that the node and file can be accessed by
+    // everyone.
+    $node->status = NODE_PUBLISHED;
+    node_save($node);
+    $this->drupalLogin($this->admin_user);
+    $this->drupalGet("node/$nid");
+    $this->assertResponse(200, 'Administrator can access the published node.');
+    $this->drupalGet($file_url);
+    $this->assertResponse(200, 'Administrator can download the file attached to the published node.');
+    $this->drupalLogOut();
+    $this->drupalGet("node/$nid");
+    $this->assertResponse(200, 'Anonymous user can access the published node.');
+    $this->drupalGet($file_url);
+    $this->assertResponse(200, 'Anonymous user can download the file attached to the published node.');
+
+    // Make the node private via the node access system and test that only
+    // administrators (not anonymous users) can access the node and download
+    // the file.
+    $node->private = TRUE;
+    node_save($node);
+    $this->drupalLogin($this->admin_user);
+    $this->drupalGet("node/$nid");
+    $this->assertResponse(200, 'Administrator can access the private node.');
+    $this->drupalGet($file_url);
+    $this->assertResponse(200, 'Administrator can download the file attached to the private node.');
+    $this->drupalLogOut();
+    $this->drupalGet("node/$nid");
+    $this->assertResponse(403, 'Anonymous user cannot access the private node.');
+    $this->drupalGet($file_url);
+    $this->assertResponse(403, 'Anonymous user cannot download the file attached to the private node.');
+  }
 }
Index: drupal7/modules/file/tests/file_module_test.module
===================================================================
--- drupal7.orig/modules/file/tests/file_module_test.module
+++ drupal7/modules/file/tests/file_module_test.module
@@ -67,3 +67,18 @@ function file_module_test_form_submit($f
   }
   drupal_set_message(t('The file id is %fid.', array('%fid' => $fid)));
 }
+
+/**
+ * Implements hook_file_download().
+ */
+function file_module_test_file_download($uri) {
+  if (variable_get('file_module_test_grant_download_access')) {
+    // Mimic what file_get_content_headers() would do if we had a full $file
+    // object to pass to it.
+    return array(
+      'Content-Type' => mime_header_encode(file_get_mimetype($uri)),
+      'Content-Length' => filesize($uri),
+      'Cache-Control' => 'private',
+    );
+  }
+}
Index: drupal7/modules/simpletest/tests/common.test
===================================================================
--- drupal7.orig/modules/simpletest/tests/common.test
+++ drupal7/modules/simpletest/tests/common.test
@@ -76,7 +76,7 @@ class DrupalAlterTestCase extends Drupal
 class CommonURLUnitTest extends DrupalWebTestCase {
   public static function getInfo() {
     return array(
-      'name' => 'URL generation tests',
+      'name' => 'URL generation unit tests',
       'description' => 'Confirm that url(), drupal_get_query_parameters(), drupal_http_build_query(), and l() work correctly with various input.',
       'group' => 'System',
     );
@@ -372,6 +372,38 @@ class CommonURLUnitTest extends DrupalWe
   }
 }
 
+/**
+ * Web tests for URL generation functions.
+ */
+class CommonURLWebTest extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'URL generation web tests',
+      'description' => 'Confirm that URL-generating functions work correctly on specific site paths.',
+      'group' => 'System',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('common_test');
+  }
+
+  /**
+   * Tests the url() function on internal paths which mimic external URLs.
+   */
+  function testInternalPathMimicsExternal() {
+    // Ensure that calling url(current_path()) on "/http://example.com" (an
+    // internal path which mimics an external URL) always links to the internal
+    // path, not the external URL. This helps protect against external URL link
+    // injection vulnerabilities.
+    variable_set('common_test_link_to_current_path', TRUE);
+    $this->drupalGet('/http://example.com');
+    $this->clickLink('link which should point to the current path');
+    $this->assertUrl('/http://example.com');
+    $this->assertText('link which should point to the current path');
+  }
+}
+
 /**
  * Tests url_is_external().
  */
Index: drupal7/modules/simpletest/tests/common_test.module
===================================================================
--- drupal7.orig/modules/simpletest/tests/common_test.module
+++ drupal7/modules/simpletest/tests/common_test.module
@@ -99,6 +99,9 @@ function common_test_init() {
   if (variable_get('common_test_redirect_current_path', FALSE)) {
     drupal_goto(current_path());
   }
+  if (variable_get('common_test_link_to_current_path', FALSE)) {
+    drupal_set_message(l('link which should point to the current path', current_path()));
+  }
 }
 
 /**
