diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 232868cfab2..1414852d443 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,35 +1,61 @@ -Drupal 6.47 LTS, 2019-01-02 +Drupal 6.52, 2019-05-08 - Long term support +--------------------------------------- +- Fixed security issues (cross site scripting), backport. See SA-CORE-2019-007. +- Fixed db_version_compare() does not handle if $db_url is an array + +Drupal 6.51, 2019-04-29 - Long term support +--------------------------------------- +- Add support for MySQL 8 + +Drupal 6.50, 2019-04-17 - Long term support +--------------------------------------- +- Fixed security issues (cross site scripting), backport. See SA-CORE-2019-006. + +Drupal 6.49, 2019-01-16 - Long term support +--------------------------------------- +- Fixes issues with some Drush commands when using the PHAR file. See + https://www.drupal.org/project/drupal/issues/3026386 + +Drupal 6.48, 2019-01-16 - Long term support +--------------------------------------- +- Fixed security issues (arbitrary PHP code execution), backport. See + SA-CORE-2019-002. + +Drupal 6.47, 2019-01-02 - Long term support --------------------------------------- - Improved support for PHP 7.2. -Drupal 6.46 LTS, 2018-10-17 +Drupal 6.46, 2018-10-17 - Long term support --------------------------------------- - Fixed security issues (open redirect), backport. See SA-CORE-2018-006. -Drupal 6.45 LTS, 2018-10-04 +Drupal 6.45, 2018-10-04 - Long term support --------------------------------------- - Initial support for PHP 7.2. -Drupal 6.44 LTS, 2018-04-25 +Drupal 6.44, 2018-04-25 - Long term support --------------------------------------- - Fixed security issues (remote code execution), backport. See SA-CORE-2018-004. -Drupal 6.43 LTS, 2018-03-29 ------------------------ -- Fixes bug from SA-CORE-2018-002 changes, update version. +Drupal 6.43, 2018-03-28 - Long term support +--------------------------------------- +- Bug fixes to changes in 6.42 that affects the OG module. -Drupal 6.41, Drupal 6.42 ------------------------ -Skipped to bring version number in line with -https://github.com/d6lts/drupal +Drupal 6.42, 2018-03-28 - Long term support +--------------------------------------- +- Fixed security issues (remote code execution), backport. See SA-CORE-2018-002. -Drupal 6.40 Pressflow, 2018-03-28 ------------------------ -- Fixed security issues (multiple vulnerabilities). See SA-CORE-2018-002. +Drupal 6.40, 2018-02-22 - Long term support +--------------------------------------- +- Bug fixes to changes in 6.39 -Drupal 6.39 Pressflow, 2018-02-21 ------------------------ -- Fixed security issues (multiple vulnerabilities). See SA-CORE-2018-001. +Drupal 6.40, 2018-02-22 - Long term support +--------------------------------------- +- Bug fixes to changes in 6.39 + +Drupal 6.39, 2018-02-21 - Long term support +--------------------------------------- +- Fixed security issues (multiple vulnerabilities), backport. See SA-CORE-2018-001. Drupal 6.38, 2016-02-24 - Final release --------------------------------------- diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index be8b4d02324..e910a0429a8 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -1479,6 +1479,18 @@ function _drupal_bootstrap($phase) { case DRUPAL_BOOTSTRAP_CONFIGURATION: drupal_unset_globals(); + // PHP's built-in phar:// stream wrapper is not sufficiently secure. Override + // it with a more secure one, which requires PHP 5.3.3. For lower versions, + // unregister the built-in one without replacing it. Sites needing phar + // support for lower PHP versions must implement hook_stream_wrappers() to + // register their desired implementation. + if (in_array('phar', stream_get_wrappers(), TRUE)) { + stream_wrapper_unregister('phar'); + if (version_compare(PHP_VERSION, '5.3.3', '>=')) { + include_once './includes/file.phar.inc'; + file_register_phar_wrapper(); + } + } // Start a page timer: timer_start('page'); // Initialize the configuration diff --git a/includes/common.inc b/includes/common.inc index de895897edd..f70e0e1ae18 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -2243,6 +2243,8 @@ function drupal_add_js($data = NULL, $type = 'module', $scope = 'header', $defer $javascript['header'] = array( 'core' => array( 'misc/jquery.js' => array('cache' => TRUE, 'defer' => FALSE, 'preprocess' => TRUE), + 'misc/jquery-extend-3.4.0.js' => array('cache' => TRUE, 'defer' => FALSE, 'preprocess' => TRUE), + 'misc/jquery-html-prefilter-3.5.0-backport.js' => array('cache' => TRUE, 'defer' => FALSE, 'preprocess' => TRUE), 'misc/drupal.js' => array('cache' => TRUE, 'defer' => FALSE, 'preprocess' => TRUE), ), 'module' => array(), @@ -3583,6 +3585,17 @@ function drupal_write_record($table, &$object, $update = array()) { } } + // Need to escape reserved keywords for MySQL 8. + if (db_version_compare('mysql', '8.0.0')) { + global $db_mysql8_reserved_keywords; + + foreach ($fields as $key => $field) { + if (in_array($field, $db_mysql8_reserved_keywords)) { + $fields[$key] = '`' . $field . '`'; + } + } + } + // Build the SQL. $query = ''; if (!count($update)) { diff --git a/includes/database.inc b/includes/database.inc index efb4a90fa25..70737da2003 100644 --- a/includes/database.inc +++ b/includes/database.inc @@ -65,6 +65,312 @@ function update_sql($sql) { return array('success' => $result !== FALSE, 'query' => check_plain($sql)); } +/** + * MySQL 8 reserved keywords list. + * + * @see https://dev.mysql.com/doc/refman/8.0/en/keywords.html + */ +global $db_mysql8_reserved_keywords; +$db_mysql8_reserved_keywords = array( + 'accessible', + 'add', + 'admin', + 'all', + 'alter', + 'analyze', + 'and', + 'as', + 'asc', + 'asensitive', + 'before', + 'between', + 'bigint', + 'binary', + 'blob', + 'both', + 'by', + 'call', + 'cascade', + 'case', + 'change', + 'char', + 'character', + 'check', + 'collate', + 'column', + 'condition', + 'constraint', + 'continue', + 'convert', + 'create', + 'cross', + 'cube', + 'cume_dist', + 'current_date', + 'current_time', + 'current_timestamp', + 'current_user', + 'cursor', + 'database', + 'databases', + 'day_hour', + 'day_microsecond', + 'day_minute', + 'day_second', + 'dec', + 'decimal', + 'declare', + 'default', + 'delayed', + 'delete', + 'dense_rank', + 'desc', + 'describe', + 'deterministic', + 'distinct', + 'distinctrow', + 'div', + 'double', + 'drop', + 'dual', + 'each', + 'else', + 'elseif', + 'empty', + 'enclosed', + 'escaped', + 'except', + 'exists', + 'exit', + 'explain', + 'false', + 'fetch', + 'first_value', + 'float', + 'float4', + 'float8', + 'for', + 'force', + 'foreign', + 'from', + 'fulltext', + 'function', + 'generated', + 'get', + 'grant', + 'group', + 'grouping', + 'groups', + 'having', + 'high_priority', + 'hour_microsecond', + 'hour_minute', + 'hour_second', + 'if', + 'ignore', + 'in', + 'index', + 'infile', + 'inner', + 'inout', + 'insensitive', + 'insert', + 'int', + 'int1', + 'int2', + 'int3', + 'int4', + 'int8', + 'integer', + 'interval', + 'into', + 'io_after_gtids', + 'io_before_gtids', + 'is', + 'iterate', + 'join', + 'json_table', + 'key', + 'keys', + 'kill', + 'lag', + 'last_value', + 'lead', + 'leading', + 'leave', + 'left', + 'like', + 'limit', + 'linear', + 'lines', + 'load', + 'localtime', + 'localtimestamp', + 'lock', + 'long', + 'longblob', + 'longtext', + 'loop', + 'low_priority', + 'master_bind', + 'master_ssl_verify_server_cert', + 'match', + 'maxvalue', + 'mediumblob', + 'mediumint', + 'mediumtext', + 'middleint', + 'minute_microsecond', + 'minute_second', + 'mod', + 'modifies', + 'natural', + 'not', + 'no_write_to_binlog', + 'nth_value', + 'ntile', + 'null', + 'numeric', + 'of', + 'on', + 'optimize', + 'optimizer_costs', + 'option', + 'optionally', + 'or', + 'order', + 'out', + 'outer', + 'outfile', + 'over', + 'partition', + 'percent_rank', + 'persist', + 'persist_only', + 'precision', + 'primary', + 'procedure', + 'purge', + 'range', + 'rank', + 'read', + 'reads', + 'read_write', + 'real', + 'recursive', + 'references', + 'regexp', + 'release', + 'rename', + 'repeat', + 'replace', + 'require', + 'resignal', + 'restrict', + 'return', + 'revoke', + 'right', + 'rlike', + 'row', + 'rows', + 'row_number', + 'schema', + 'schemas', + 'second_microsecond', + 'select', + 'sensitive', + 'separator', + 'set', + 'show', + 'signal', + 'smallint', + 'spatial', + 'specific', + 'sql', + 'sqlexception', + 'sqlstate', + 'sqlwarning', + 'sql_big_result', + 'sql_calc_found_rows', + 'sql_small_result', + 'ssl', + 'starting', + 'stored', + 'straight_join', + 'system', + 'table', + 'terminated', + 'then', + 'tinyblob', + 'tinyint', + 'tinytext', + 'to', + 'trailing', + 'trigger', + 'true', + 'undo', + 'union', + 'unique', + 'unlock', + 'unsigned', + 'update', + 'usage', + 'use', + 'using', + 'utc_date', + 'utc_time', + 'utc_timestamp', + 'values', + 'varbinary', + 'varchar', + 'varcharacter', + 'varying', + 'virtual', + 'when', + 'where', + 'while', + 'window', + 'with', + 'write', + 'xor', + 'year_month', + 'zerofill', +); + +/** + * Checks if we are under certain database driver and database version. + * + * For example, to check if we are under MySQL 8.0 or higher use + * db_version_compare('mysql', '8.0.0'). + * + * @param string $driver + * Database driver name from $db_url setting. For example 'mysql' (which covers both 'mysql:' and 'mysqli:' drivers. + * @param string $version + * Database version returned by db_version() function call. + * @param string $operator + * Operator to compare version. '>=' by default. + * @return bool + * TRUE if version check passes; otherwise FALSE. +*/ +function db_version_compare($driver, $version, $operator = '>=') { + global $db_url, $db_active_name; + + if (is_array($db_url)) { + $db_active_url = array_key_exists($db_active_name, $db_url) ? $db_url[$db_active_name] : $db_url['default']; + } + else { + $db_active_url = $db_url; + } + + if (substr($db_active_url, 0, strlen($driver)) === $driver) { + if (version_compare(db_version(), $version, $operator)) { + return TRUE; + } + } + + return FALSE; +} + /** * Append a database prefix to all tables in a query. * @@ -79,29 +385,48 @@ function update_sql($sql) { * The properly-prefixed string. */ function db_prefix_tables($sql) { - global $db_prefix; + global $db_prefix, $db_mysql8_reserved_keywords; + // Add prefixes. if (is_array($db_prefix)) { if (array_key_exists('default', $db_prefix)) { $tmp = $db_prefix; unset($tmp['default']); + foreach ($tmp as $key => $val) { - $sql = strtr($sql, array('{'. $key .'}' => $val . $key)); + $sql = strtr($sql, array('{' . $key . '}' => '{' . $val . $key . '}')); } - return strtr($sql, array('{' => $db_prefix['default'], '}' => '')); + $sql = strtr($sql, array('{' => '{' . $db_prefix['default'])); } else { foreach ($db_prefix as $key => $val) { - $sql = strtr($sql, array('{'. $key .'}' => $val . $key)); + $sql = strtr($sql, array('{' . $key . '}' => '{' . $val . $key . '}')); } - return strtr($sql, array('{' => '', '}' => '')); } } else { - return strtr($sql, array('{' => $db_prefix, '}' => '')); + $sql = strtr($sql, array('{' => '{' . $db_prefix)); } + + // Need to escape reserved keywords for MySQL 8. + if (db_version_compare('mysql', '8.0.0')) { + foreach ($db_mysql8_reserved_keywords as $keyword) { + $sql = str_replace('{' . $keyword . '}', '`{' . $keyword . '}`', $sql); + } + } + + // Remove curly braces. + $sql = strtr($sql, array('{' => '', '}' => '')); + + return $sql; } +/** + * Active database connection name. + */ +global $db_active_name; +$db_active_name = FALSE; + /** * Activate a database for future queries. * @@ -123,7 +448,8 @@ function db_prefix_tables($sql) { */ function db_set_active($name = 'default') { global $db_url, $db_slave_url, $db_type, $active_db, $active_slave_db; - static $db_conns, $db_slave_conns, $active_name = FALSE; + global $db_active_name; + static $db_conns, $db_slave_conns; if (empty($db_url)) { include_once 'includes/install.inc'; @@ -169,9 +495,9 @@ function db_set_active($name = 'default') { } } - $previous_name = $active_name; + $previous_name = $db_active_name; // Set the active connection. - $active_name = $name; + $db_active_name = $name; $active_db = $db_conns[$name]; if (isset($db_slave_conns[$name])) { $active_slave_db = $db_slave_conns[$name]; diff --git a/includes/database.mysql.inc b/includes/database.mysql.inc index 4ff53e1fa77..bed91fb0aff 100644 --- a/includes/database.mysql.inc +++ b/includes/database.mysql.inc @@ -89,6 +89,13 @@ function db_connect($url) { mysql_query('SET NAMES utf8', $connection); } + // MySQL 8 has ONLY_FULL_GROUP_BY mode enabled by default. There are too many queries in Drupal 6 that do not + // list all columns in GROUP BY clause. So we need to turn off this mode after connecting. + list ($version) = explode('-', mysql_get_server_info($connection)); + if (version_compare($version, '8.0.0', '>=')){ + mysql_query('SET sql_mode=(SELECT REPLACE(@@sql_mode,\'ONLY_FULL_GROUP_BY\',\'\'));', $connection); + } + return $connection; } diff --git a/includes/database.mysqli.inc b/includes/database.mysqli.inc index 2ee710bdb75..050614a1df7 100644 --- a/includes/database.mysqli.inc +++ b/includes/database.mysqli.inc @@ -88,6 +88,13 @@ function db_connect($url) { mysqli_query($connection, 'SET NAMES utf8'); } + // MySQL 8 has ONLY_FULL_GROUP_BY mode enabled by default. There are too many queries in Drupal 6 that do not + // list all columns in GROUP BY clause. So we need to turn off this mode after connecting. + list ($version) = explode('-', mysqli_get_server_info($connection)); + if (version_compare($version, '8.0.0', '>=')){ + mysqli_query($connection, 'SET sql_mode=(SELECT REPLACE(@@sql_mode,\'ONLY_FULL_GROUP_BY\',\'\'));'); + } + return $connection; } diff --git a/includes/file.inc b/includes/file.inc index bfa3583890d..c091b7f2754 100644 --- a/includes/file.inc +++ b/includes/file.inc @@ -704,7 +704,7 @@ function file_save_upload($source, $validators = array(), $dest = FALSE, $replac } // Rename potentially executable files, to help prevent exploits. - if (preg_match('/\.(php|pl|py|cgi|asp|js)$/i', $file->filename) && (substr($file->filename, -4) != '.txt')) { + if (preg_match('/\.(php|phar|pl|py|cgi|asp|js)$/i', $file->filename) && (substr($file->filename, -4) != '.txt')) { $file->filemime = 'text/plain'; $file->filepath .= '.txt'; $file->filename .= '.txt'; diff --git a/includes/file.phar.inc b/includes/file.phar.inc new file mode 100644 index 00000000000..f3b24d332f8 --- /dev/null +++ b/includes/file.phar.inc @@ -0,0 +1,55 @@ +withAssertion(new PharExtensionInterceptor()) + ); + } + catch (\LogicException $e) { + // Continue if the PharStreamWrapperManager is already initialized. + // For example, this occurs following a drupal_static_reset(), such + // as during tests. + }; + + // To prevent file_stream_wrapper_valid_scheme() treating "phar" as a valid + // scheme, this is registered with PHP only, not with hook_stream_wrappers() + // or the internal storage of file_get_stream_wrappers(). + stream_wrapper_register('phar', '\\TYPO3\\PharStreamWrapper\\PharStreamWrapper'); +} diff --git a/includes/session.inc b/includes/session.inc index 278693864d9..0cea8082669 100644 --- a/includes/session.inc +++ b/includes/session.inc @@ -180,6 +180,8 @@ function sess_destroy_sid($sid) { unset($_COOKIE[session_name()]); } } + + return TRUE; } /** @@ -190,6 +192,7 @@ function sess_destroy_sid($sid) { */ function sess_destroy_uid($uid) { db_query('DELETE FROM {sessions} WHERE uid = %d', $uid); + return TRUE; } function sess_gc($lifetime) { diff --git a/misc/brumann/polyfill-unserialize/.gitignore b/misc/brumann/polyfill-unserialize/.gitignore new file mode 100644 index 00000000000..767699f1b85 --- /dev/null +++ b/misc/brumann/polyfill-unserialize/.gitignore @@ -0,0 +1,4 @@ +/vendor/ +/phpunit.xml +/.composer.lock + diff --git a/misc/brumann/polyfill-unserialize/.travis.yml b/misc/brumann/polyfill-unserialize/.travis.yml new file mode 100644 index 00000000000..352536f4584 --- /dev/null +++ b/misc/brumann/polyfill-unserialize/.travis.yml @@ -0,0 +1,20 @@ +language: php + +sudo: false + +php: + - '5.3' + - '5.4' + - '5.5' + - '5.6' + - '7.0' + - '7.1' + +before_install: + - phpenv config-rm xdebug.ini + - composer self-update + +install: + - composer install + +script: phpunit diff --git a/misc/brumann/polyfill-unserialize/LICENSE b/misc/brumann/polyfill-unserialize/LICENSE new file mode 100644 index 00000000000..0cb53d3b026 --- /dev/null +++ b/misc/brumann/polyfill-unserialize/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Denis Brumann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/misc/brumann/polyfill-unserialize/README.md b/misc/brumann/polyfill-unserialize/README.md new file mode 100644 index 00000000000..bac25fe049c --- /dev/null +++ b/misc/brumann/polyfill-unserialize/README.md @@ -0,0 +1,61 @@ +Polyfill unserialize [![Build Status](https://travis-ci.org/dbrumann/polyfill-unserialize.svg?branch=master)](https://travis-ci.org/dbrumann/polyfill-unserialize) +=== + +Backports unserialize options introduced in PHP 7.0 to older PHP versions. +This was originally designed as a Proof of Concept for Symfony Issue [#21090](https://github.com/symfony/symfony/pull/21090). + +You can use this package in projects that rely on PHP versions older than PHP 7.0. +In case you are using PHP 7.0+ the original `unserialize()` will be used instead. + +From the [documentation](https://secure.php.net/manual/en/function.unserialize.php): + +> Warning: Do not pass untrusted user input to unserialize(). Unserialization can +> result in code being loaded and executed due to object instantiation +> and autoloading, and a malicious user may be able to exploit this. + +This warning holds true even when `allowed_classes` is used. + +Requirements +------------ + + - PHP 5.3+ + +Installation +------------ + +You can install this package via composer: + +``` +composer require brumann/polyfill-unserialize "^1.0" +``` + +Known Issues +------------ + +There is a mismatch in behavior when `allowed_classes` in `$options` is not +of the correct type (array or boolean). PHP 7.1 will issue a warning, whereas +PHP 7.0 will not. I opted to copy the behavior of the former. + +Tests +----- + +You can run the test suite using PHPUnit. It is intentionally not bundled as +dev dependency to make sure this package has the lowest restrictions on the +implementing system as possible. + +Please read the [PHPUnit Manual](https://phpunit.de/manual/current/en/installation.html) +for information how to install it on your system. + +You can run the test suite as follows: + +``` +phpunit -c phpunit.xml.dist tests/ +``` + +Contributing +------------ + +This package is considered feature complete. As such I will likely not update it +unless there are security issues. + +Should you find any bugs or have questions, feel free to submit an Issue or a Pull Request. diff --git a/misc/brumann/polyfill-unserialize/composer.json b/misc/brumann/polyfill-unserialize/composer.json new file mode 100644 index 00000000000..ec4a2cf0eab --- /dev/null +++ b/misc/brumann/polyfill-unserialize/composer.json @@ -0,0 +1,26 @@ +{ + "name": "brumann/polyfill-unserialize", + "description": "Backports unserialize options introduced in PHP 7.0 to older PHP versions.", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Denis Brumann", + "email": "denis.brumann@sensiolabs.de" + } + ], + "autoload": { + "psr-4": { + "Brumann\\Polyfill\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\Brumann\\Polyfill\\": "tests/" + } + }, + "minimum-stability": "stable", + "require": { + "php": "^5.3|^7.0" + } +} diff --git a/misc/brumann/polyfill-unserialize/phpunit.xml.dist b/misc/brumann/polyfill-unserialize/phpunit.xml.dist new file mode 100644 index 00000000000..8fea1bab869 --- /dev/null +++ b/misc/brumann/polyfill-unserialize/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + + + + + + ./tests/ + + + + + + ./src/ + + + diff --git a/misc/brumann/polyfill-unserialize/src/Unserialize.php b/misc/brumann/polyfill-unserialize/src/Unserialize.php new file mode 100644 index 00000000000..e025d55ed4e --- /dev/null +++ b/misc/brumann/polyfill-unserialize/src/Unserialize.php @@ -0,0 +1,58 @@ += 70000) { + return \unserialize($serialized, $options); + } + if (!array_key_exists('allowed_classes', $options)) { + $options['allowed_classes'] = true; + } + $allowedClasses = $options['allowed_classes']; + if (true === $allowedClasses) { + return \unserialize($serialized); + } + if (false === $allowedClasses) { + $allowedClasses = array(); + } + if (!is_array($allowedClasses)) { + trigger_error( + 'unserialize(): allowed_classes option should be array or boolean', + E_USER_WARNING + ); + $allowedClasses = array(); + } + + $sanitizedSerialized = preg_replace_callback( + '/(^|;)O:\d+:"([^"]*)":(\d+):{/', + function ($match) use ($allowedClasses) { + list($completeMatch, $leftBorder, $className, $objectSize) = $match; + if (in_array($className, $allowedClasses)) { + return $completeMatch; + } else { + return sprintf( + '%sO:22:"__PHP_Incomplete_Class":%d:{s:27:"__PHP_Incomplete_Class_Name";%s', + $leftBorder, + $objectSize + 1, // size of object + 1 for added string + \serialize($className) + ); + } + }, + $serialized + ); + + return \unserialize($sanitizedSerialized); + } +} diff --git a/misc/jquery-extend-3.4.0.js b/misc/jquery-extend-3.4.0.js new file mode 100644 index 00000000000..179c097e8e8 --- /dev/null +++ b/misc/jquery-extend-3.4.0.js @@ -0,0 +1,174 @@ +/** + * For jQuery versions less than 3.4.0, this replaces the jQuery.extend + * function with the one from jQuery 3.4.0, slightly modified (documented + * below) to be compatible with older jQuery versions and browsers. + * + * This provides the Object.prototype pollution vulnerability fix to Drupal + * installations running older jQuery versions, including the versions shipped + * with Drupal core and https://www.drupal.org/project/jquery_update. + * + * @see https://github.com/jquery/jquery/pull/4333 + */ + +(function (jQuery) { + +// Do not override jQuery.extend() if the jQuery version is already >=3.4.0. +var versionParts = jQuery.fn.jquery.split('.'); +var majorVersion = parseInt(versionParts[0]); +var minorVersion = parseInt(versionParts[1]); +var patchVersion = parseInt(versionParts[2]); +var isPreReleaseVersion = (patchVersion.toString() !== versionParts[2]); +if ( + (majorVersion > 3) || + (majorVersion === 3 && minorVersion > 4) || + (majorVersion === 3 && minorVersion === 4 && patchVersion > 0) || + (majorVersion === 3 && minorVersion === 4 && patchVersion === 0 && !isPreReleaseVersion) +) { + return; +} + +/** + * This adds some funtions from jQuery 1.4.4 (the default version used in + * Drupal 7) for when they aren't present, like when using jQuery 1.2.6 (the + * default version used in Drupal 6). + */ + +if (typeof jQuery.type === 'undefined') { + var toString = Object.prototype.toString, + class2type = {}; + + // Populate the class2type map + jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); + }); + + jQuery.type = function (obj) { + return obj == null ? + String( obj ) : + class2type[ toString.call(obj) ] || "object"; + }; +} + +if (typeof jQuery.isArray === 'undefined') { + jQuery.isArray = function (obj) { + return jQuery.type(obj) === "array"; + }; +} + +if (typeof jQuery.isWindow === 'undefined') { + jQuery.isWindow = function (obj) { + return obj && typeof obj === "object" && "setInterval" in obj; + }; +} + +if (typeof jQuery.isPlainObject === 'undefined') { + var hasOwn = Object.prototype.hasOwnProperty; + + jQuery.isPlainObject = function (obj) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }; +} + +/** + * This is almost verbatim copied from jQuery 3.4.0. + * + * Only two minor changes have been made: + * - The call to isFunction() is changed to jQuery.isFunction(). + * - The two calls to Array.isArray() is changed to jQuery.isArray(). + * + * The above two changes ensure compatibility with all older jQuery versions + * (1.2.6 - 3.3.1) and older browser versions (e.g., IE8). + */ +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + copy = options[ name ]; + + // Prevent Object.prototype pollution + // Prevent never-ending loop + if ( name === "__proto__" || target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = jQuery.isArray( copy ) ) ) ) { + src = target[ name ]; + + // Ensure proper type for the source value + if ( copyIsArray && !jQuery.isArray( src ) ) { + clone = []; + } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { + clone = {}; + } else { + clone = src; + } + copyIsArray = false; + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +})(jQuery); diff --git a/misc/jquery-html-prefilter-3.5.0-backport.js b/misc/jquery-html-prefilter-3.5.0-backport.js new file mode 100644 index 00000000000..93771502210 --- /dev/null +++ b/misc/jquery-html-prefilter-3.5.0-backport.js @@ -0,0 +1,251 @@ +/** + * For jQuery versions less than 3.5.0, this replaces the jQuery.htmlPrefilter() + * function with one that fixes these security vulnerabilities while also + * retaining the pre-3.5.0 behavior where it's safe to do so. + * - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022 + * - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023 + * + * Additionally, for jQuery versions that do not have a jQuery.htmlPrefilter() + * function (1.x prior to 1.12 and 2.x prior to 2.2), this adds it, and + * extends the functions that need to call it to do so. + * + * Drupal core's jQuery version is 1.4.4, but jQuery Update can provide a + * different version, so this covers all versions between 1.4.4 and 3.4.1. + * The GitHub links in the code comments below link to jQuery 1.5 code, because + * 1.4.4 isn't on GitHub, but the referenced code didn't change from 1.4.4 to + * 1.5. + */ + +(function (jQuery) { + + // Parts of this backport differ by jQuery version. + var versionParts = jQuery.fn.jquery.split('.'); + var majorVersion = parseInt(versionParts[0]); + var minorVersion = parseInt(versionParts[1]); + + // No backport is needed if we're already on jQuery 3.5 or higher. + if ( (majorVersion > 3) || (majorVersion === 3 && minorVersion >= 5) ) { + return; + } + + // Prior to jQuery 3.5, jQuery converted XHTML-style self-closing tags to + // their XML equivalent: e.g., "
" to "
". This is + // problematic for several reasons, including that it's vulnerable to XSS + // attacks. However, since this was jQuery's behavior for many years, many + // Drupal modules and jQuery plugins may be relying on it. Therefore, we + // preserve that behavior, but for a limited set of tags only, that we believe + // to not be vulnerable. This is the set of HTML tags that satisfy all of the + // following conditions: + // - In DOMPurify's list of HTML tags. If an HTML tag isn't safe enough to + // appear in that list, then we don't want to mess with it here either. + // @see https://github.com/cure53/DOMPurify/blob/2.0.11/dist/purify.js#L128 + // - A normal element (not a void, template, text, or foreign element). + // @see https://html.spec.whatwg.org/multipage/syntax.html#elements-2 + // - An element that is still defined by the current HTML specification + // (not a deprecated element), because we do not want to rely on how + // browsers parse deprecated elements. + // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element + // - Not 'html', 'head', or 'body', because this pseudo-XHTML expansion is + // designed for fragments, not entire documents. + // - Not 'colgroup', because due to an idiosyncrasy of jQuery's original + // regular expression, it didn't match on colgroup, and we don't want to + // introduce a behavior change for that. + var selfClosingTagsToReplace = [ + 'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', + 'blockquote', 'button', 'canvas', 'caption', 'cite', 'code', 'data', + 'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em', + 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', + 'h4', 'h5', 'h6', 'header', 'hgroup', 'i', 'ins', 'kbd', 'label', 'legend', + 'li', 'main', 'map', 'mark', 'menu', 'meter', 'nav', 'ol', 'optgroup', + 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', + 'ruby', 's', 'samp', 'section', 'select', 'small', 'source', 'span', + 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', + 'thead', 'time', 'tr', 'u', 'ul', 'var', 'video' + ]; + + // Define regular expressions for and . Doing this as + // two expressions makes it easier to target without also targeting + // every tag that starts with "a". + var xhtmlRegExpGroup = '(' + selfClosingTagsToReplace.join('|') + ')'; + var whitespace = '[\\x20\\t\\r\\n\\f]'; + var rxhtmlTagWithoutSpaceOrAttributes = new RegExp('<' + xhtmlRegExpGroup + '\\/>', 'gi'); + var rxhtmlTagWithSpaceAndMaybeAttributes = new RegExp('<' + xhtmlRegExpGroup + '(' + whitespace + '[^>]*)\\/>', 'gi'); + + // jQuery 3.5 also fixed a vulnerability for when appears within + // an , but it did that in local code that we can't + // backport directly. Instead, we filter such cases out. To do so, we need to + // determine when jQuery would otherwise invoke the vulnerable code, which it + // uses this regular expression to determine. The regular expression changed + // for version 3.0.0 and changed again for 3.4.0. + // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4958 + // @see https://github.com/jquery/jquery/blob/3.0.0/dist/jquery.js#L4584 + // @see https://github.com/jquery/jquery/blob/3.4.0/dist/jquery.js#L4712 + var rtagName; + if (majorVersion < 3) { + rtagName = /<([\w:]+)/; + } + else if (minorVersion < 4) { + rtagName = /<([a-z][^\/\0>\x20\t\r\n\f]+)/i; + } + else { + rtagName = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i; + } + + // The regular expression that jQuery uses to determine which self-closing + // tags to expand to open and close tags. This is vulnerable, because it + // matches all tag names except the few excluded ones. We only use this + // expression for determining vulnerability. The expression changed for + // version 3, but we only need to check for vulnerability in versions 1 and 2, + // so we use the expression from those versions. + // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4957 + var rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi; + + jQuery.extend({ + htmlPrefilter: function (html) { + // This is how jQuery determines the first tag in the HTML. + // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5521 + var tag = ( rtagName.exec( html ) || [ "", "" ] )[ 1 ].toLowerCase(); + + // It is not valid HTML for to have