From 855aaa3c632faa38e408ed45917fa9ff0b8e3269 Mon Sep 17 00:00:00 2001 From: David Snopek Date: Wed, 2 Jan 2019 23:13:16 -0600 Subject: [PATCH 1/5] Apply the changes from D6LTS 6.47 --- CHANGELOG.txt | 4 ++++ includes/form.inc | 13 +++++++------ includes/theme.inc | 10 +++++----- includes/unicode.inc | 14 +++++++++----- modules/book/book.module | 3 ++- modules/profile/profile.module | 1 + modules/system/system.module | 2 +- 7 files changed, 29 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index caa00daabcd..232868cfab2 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,7 @@ +Drupal 6.47 LTS, 2019-01-02 +--------------------------------------- +- Improved support for PHP 7.2. + Drupal 6.46 LTS, 2018-10-17 --------------------------------------- - Fixed security issues (open redirect), backport. See SA-CORE-2018-006. diff --git a/includes/form.inc b/includes/form.inc index be58955e2b4..272c0f5087e 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -1424,14 +1424,15 @@ function form_set_value($form_item, $value, &$form_state) { * the right array. */ function _form_set_value(&$form_values, $form_item, $parents, $value) { + // This makes PHP 7 have the same behavior as PHP 5 when the value is an + // empty string, rather than an array. This is depended on surprisingly + // often in Drupal 6 contrib. + if ($form_values === '') { + $form_values = array(); + } + $parent = array_shift($parents); if (empty($parents)) { - // This makes PHP 7 have the same behavior as PHP 5 when the value is an - // empty string, rather than an array. This is depended on surprisingly - // often in Drupal 6 contrib. - if ($form_values === '') { - $form_values = array(); - } $form_values[$parent] = $value; } else { diff --git a/includes/theme.inc b/includes/theme.inc index 0b25f710e54..21e203e36af 100644 --- a/includes/theme.inc +++ b/includes/theme.inc @@ -1371,7 +1371,7 @@ function theme_submenu($links) { function theme_table($header, $rows, $attributes = array(), $caption = NULL) { // Add sticky headers, if applicable. - if (count($header)) { + if (!empty($header)) { drupal_add_js('misc/tableheader.js'); // Add 'sticky-enabled' class to the table to identify it for JS. // This is needed to target tables constructed by this function. @@ -1385,24 +1385,24 @@ function theme_table($header, $rows, $attributes = array(), $caption = NULL) { } // Format the table header: - if (count($header)) { + if (!empty($header)) { $ts = tablesort_init($header); // HTML requires that the thead tag has tr tags in it followed by tbody // tags. Using ternary operator to check and see if we have any rows. - $output .= (count($rows) ? ' ' : ' '); + $output .= (!empty($rows) ? ' ' : ' '); foreach ($header as $cell) { $cell = tablesort_header($cell, $header, $ts); $output .= _theme_table_cell($cell, TRUE); } // Using ternary operator to close the tags based on whether or not there are rows - $output .= (count($rows) ? " \n" : "\n"); + $output .= (!empty($rows) ? " \n" : "\n"); } else { $ts = array(); } // Format the table rows: - if (count($rows)) { + if (!empty($rows)) { $output .= "\n"; $flip = array('even' => 'odd', 'odd' => 'even'); $class = 'even'; diff --git a/includes/unicode.inc b/includes/unicode.inc index bf0ae52c93f..b5fe688cf0f 100644 --- a/includes/unicode.inc +++ b/includes/unicode.inc @@ -59,11 +59,15 @@ function _unicode_check() { if (ini_get('mbstring.encoding_translation') != 0) { return array(UNICODE_ERROR, $t('Multibyte string input conversion in PHP is active and must be disabled. Check the php.ini mbstring.encoding_translation setting. Please refer to the PHP mbstring documentation for more information.', array('@url' => 'http://www.php.net/mbstring'))); } - if (ini_get('mbstring.http_input') != 'pass') { - return array(UNICODE_ERROR, $t('Multibyte string input conversion in PHP is active and must be disabled. Check the php.ini mbstring.http_input setting. Please refer to the PHP mbstring documentation for more information.', array('@url' => 'http://www.php.net/mbstring'))); - } - if (ini_get('mbstring.http_output') != 'pass') { - return array(UNICODE_ERROR, $t('Multibyte string output conversion in PHP is active and must be disabled. Check the php.ini mbstring.http_output setting. Please refer to the PHP mbstring documentation for more information.', array('@url' => 'http://www.php.net/mbstring'))); + // mbstring.http_input and mbstring.http_output are deprecated and empty by + // default in PHP 5.6. + if (version_compare(PHP_VERSION, '5.6.0') == -1) { + if (ini_get('mbstring.http_input') != 'pass') { + return array(UNICODE_ERROR, $t('Multibyte string input conversion in PHP is active and must be disabled. Check the php.ini mbstring.http_input setting. Please refer to the PHP mbstring documentation for more information.', array('@url' => 'http://www.php.net/mbstring'))); + } + if (ini_get('mbstring.http_output') != 'pass') { + return array(UNICODE_ERROR, $t('Multibyte string output conversion in PHP is active and must be disabled. Check the php.ini mbstring.http_output setting. Please refer to the PHP mbstring documentation for more information.', array('@url' => 'http://www.php.net/mbstring'))); + } } // Set appropriate configuration diff --git a/modules/book/book.module b/modules/book/book.module index 56f839a33c6..37d8c06f135 100644 --- a/modules/book/book.module +++ b/modules/book/book.module @@ -556,7 +556,8 @@ function book_prev($book_link) { // The previous page in the book may be a child of the previous visible link. if ($prev['depth'] == $book_link['depth'] && $prev['has_children']) { // The subtree will have only one link at the top level - get its data. - $data = array_shift(book_menu_subtree_data($prev)); + $subtree = book_menu_subtree_data($prev); + $data = array_shift($subtree); // The link of interest is the last child - iterate to find the deepest one. while ($data['below']) { $data = end($data['below']); diff --git a/modules/profile/profile.module b/modules/profile/profile.module index e8c053904b5..e056f68dec7 100644 --- a/modules/profile/profile.module +++ b/modules/profile/profile.module @@ -490,6 +490,7 @@ function template_preprocess_profile_block(&$variables) { // Supply filtered version of $fields that have values. foreach ($variables['fields'] as $field) { if ($field->value) { + $variables['profile'][$field->name] = new stdClass(); $variables['profile'][$field->name]->title = check_plain($field->title); $variables['profile'][$field->name]->value = $field->value; $variables['profile'][$field->name]->type = $field->type; diff --git a/modules/system/system.module b/modules/system/system.module index 51771c9e4a7..6f8561ebacb 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '6.46'); +define('VERSION', '6.47'); /** * Core API compatibility. From 4c1fd4917e4c73ea1ffea575038a3d91d7822d50 Mon Sep 17 00:00:00 2001 From: David Snopek Date: Mon, 18 Feb 2019 11:05:36 -0600 Subject: [PATCH 2/5] Apply the changes from D6LTS 6.48 --- includes/bootstrap.inc | 12 + includes/file.inc | 2 +- includes/file.phar.inc | 41 ++ .../PharExtensionInterceptor.php | 73 +++ misc/typo3/phar-stream-wrapper/LICENSE | 21 + misc/typo3/phar-stream-wrapper/README.md | 155 ++++++ misc/typo3/phar-stream-wrapper/composer.json | 24 + .../phar-stream-wrapper/src/Assertable.php | 22 + .../phar-stream-wrapper/src/Behavior.php | 124 +++++ .../phar-stream-wrapper/src/Exception.php | 16 + misc/typo3/phar-stream-wrapper/src/Helper.php | 183 +++++++ .../Interceptor/PharExtensionInterceptor.php | 55 ++ .../typo3/phar-stream-wrapper/src/Manager.php | 85 ++++ .../src/PharStreamWrapper.php | 477 ++++++++++++++++++ modules/system/system.module | 2 +- 15 files changed, 1290 insertions(+), 2 deletions(-) create mode 100644 includes/file.phar.inc create mode 100644 misc/typo3/drupal-security/PharExtensionInterceptor.php create mode 100644 misc/typo3/phar-stream-wrapper/LICENSE create mode 100644 misc/typo3/phar-stream-wrapper/README.md create mode 100644 misc/typo3/phar-stream-wrapper/composer.json create mode 100644 misc/typo3/phar-stream-wrapper/src/Assertable.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Behavior.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Exception.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Helper.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Interceptor/PharExtensionInterceptor.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Manager.php create mode 100644 misc/typo3/phar-stream-wrapper/src/PharStreamWrapper.php 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/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..0e198901c87 --- /dev/null +++ b/includes/file.phar.inc @@ -0,0 +1,41 @@ +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/misc/typo3/drupal-security/PharExtensionInterceptor.php b/misc/typo3/drupal-security/PharExtensionInterceptor.php new file mode 100644 index 00000000000..a77e9f84c26 --- /dev/null +++ b/misc/typo3/drupal-security/PharExtensionInterceptor.php @@ -0,0 +1,73 @@ +baseFileContainsPharExtension($path)) { + return TRUE; + } + throw new Exception( + sprintf( + 'Unexpected file extension in "%s"', + $path + ), + 1535198703 + ); + } + + /** + * @param string $path + * The path of the phar file to check. + * + * @return bool + * TRUE if the file has a .phar extension or if the execution has been + * invoked by the phar file. + */ + private function baseFileContainsPharExtension($path) { + $baseFile = Helper::determineBaseFile($path); + if ($baseFile === NULL) { + return FALSE; + } + // If the stream wrapper is registered by invoking a phar file that does + // not not have .phar extension then this should be allowed. For + // example, some CLI tools recommend removing the extension. + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $caller = array_pop($backtrace); + if (isset($caller['file']) && $baseFile === $caller['file']) { + return TRUE; + } + $fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION); + return strtolower($fileExtension) === 'phar'; + } + +} diff --git a/misc/typo3/phar-stream-wrapper/LICENSE b/misc/typo3/phar-stream-wrapper/LICENSE new file mode 100644 index 00000000000..d71267a1adb --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 TYPO3 project - https://typo3.org/ + +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/typo3/phar-stream-wrapper/README.md b/misc/typo3/phar-stream-wrapper/README.md new file mode 100644 index 00000000000..b632784bdda --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/README.md @@ -0,0 +1,155 @@ +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/TYPO3/phar-stream-wrapper/badges/quality-score.png?b=v2)](https://scrutinizer-ci.com/g/TYPO3/phar-stream-wrapper/?branch=v2) +[![Travis CI Build Status](https://travis-ci.org/TYPO3/phar-stream-wrapper.svg?branch=v2)](https://travis-ci.org/TYPO3/phar-stream-wrapper) + +# PHP Phar Stream Wrapper + +## Abstract & History + +Based on Sam Thomas' findings concerning +[insecure deserialization in combination with obfuscation strategies](https://blog.secarma.co.uk/labs/near-phar-dangerous-unserialization-wherever-you-are) +allowing to hide Phar files inside valid image resources, the TYPO3 project +decided back then to introduce a `PharStreamWrapper` to intercept invocations +of the `phar://` stream in PHP and only allow usage for defined locations in +the file system. + +Since the TYPO3 mission statement is **inspiring people to share**, we thought +it would be helpful for others to release our `PharStreamWrapper` as standalone +package to the PHP community. + +The mentioned security issue was reported to TYPO3 on 10th June 2018 by Sam Thomas +and has been addressed concerning the specific attack vector and for this generic +`PharStreamWrapper` in TYPO3 versions 7.6.30 LTS, 8.7.17 LTS and 9.3.1 on 12th +July 2018. + +* https://typo3.org/security/advisory/typo3-core-sa-2018-002/ +* https://blog.secarma.co.uk/labs/near-phar-dangerous-unserialization-wherever-you-are +* https://youtu.be/GePBmsNJw6Y + +## License + +In general the TYPO3 core is released under the GNU General Public License version +2 or any later version (`GPL-2.0-or-later`). In order to avoid licensing issues and +incompatibilities this `PharStreamWrapper` is licenced under the MIT License. In case +you duplicate or modify source code, credits are not required but really appreciated. + +## Credits + +Thanks to [Alex Pott](https://github.com/alexpott), Drupal for creating +back-ports of all sources in order to provide compatibility with PHP v5.3. + +## Installation + +The `PharStreamWrapper` is provided as composer package `typo3/phar-stream-wrapper` +and has minimum requirements of PHP v5.3 ([`v2`](https://github.com/TYPO3/phar-stream-wrapper/tree/v2) branch) and PHP v7.0 ([`master`](https://github.com/TYPO3/phar-stream-wrapper) branch). + +### Installation for PHP v7.0 + +``` +composer require typo3/phar-stream-wrapper ^3.0 +``` + +### Installation for PHP v5.3 + +``` +composer require typo3/phar-stream-wrapper ^2.0 +``` + +## Example + +The following example is bundled within this package, the shown +`PharExtensionInterceptor` denies all stream wrapper invocations files +not having the `.phar` suffix. Interceptor logic has to be individual and +adjusted to according requirements. + +``` +$behavior = new \TYPO3\PharStreamWrapper\Behavior(); +Manager::initialize( + $behavior->withAssertion(new PharExtensionInterceptor()) +); + +if (in_array('phar', stream_get_wrappers())) { + stream_wrapper_unregister('phar'); + stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper'); +} +``` + +* `PharStreamWrapper` defined as class reference will be instantiated each time + `phar://` streams shall be processed. +* `Manager` as singleton pattern being called by `PharStreamWrapper` instances + in order to retrieve individual behavior and settings. +* `Behavior` holds reference to interceptor(s) that shall assert correct/allowed + invocation of a given `$path` for a given `$command`. Interceptors implement + the interface `Assertable`. Interceptors can act individually on following + commands or handle all of them in case not defined specifically: + + `COMMAND_DIR_OPENDIR` + + `COMMAND_MKDIR` + + `COMMAND_RENAME` + + `COMMAND_RMDIR` + + `COMMAND_STEAM_METADATA` + + `COMMAND_STREAM_OPEN` + + `COMMAND_UNLINK` + + `COMMAND_URL_STAT` + +## Interceptor + +The following interceptor is shipped with the package and ready to use in order +to block any Phar invocation of files not having a `.phar` suffix. Besides that +individual interceptors are possible of course. + +``` +class PharExtensionInterceptor implements Assertable +{ + /** + * Determines whether the base file name has a ".phar" suffix. + * + * @param string $path + * @param string $command + * @return bool + * @throws Exception + */ + public function assert($path, $command) + { + if ($this->baseFileContainsPharExtension($path)) { + return true; + } + throw new Exception( + sprintf( + 'Unexpected file extension in "%s"', + $path + ), + 1535198703 + ); + } + + /** + * @param string $path + * @return bool + */ + private function baseFileContainsPharExtension($path) + { + $baseFile = Helper::determineBaseFile($path); + if ($baseFile === null) { + return false; + } + $fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION); + return strtolower($fileExtension) === 'phar'; + } +} +``` + +## Helper + +* `Helper::determineBaseFile(string $path)`: Determines base file that can be + accessed using the regular file system. For instance the following path + `phar:///home/user/bundle.phar/content.txt` would be resolved to + `/home/user/bundle.phar`. +* `Helper::resetOpCache()`: Resets PHP's OPcache if enabled as work-around for + issues in `include()` or `require()` calls and OPcache delivering wrong + results. More details can be found in PHP's bug tracker, for instance like + https://bugs.php.net/bug.php?id=66569 + +## Security Contact + +In case of finding additional security issues in the TYPO3 project or in this +`PharStreamWrapper` package in particular, please get in touch with the +[TYPO3 Security Team](mailto:security@typo3.org). diff --git a/misc/typo3/phar-stream-wrapper/composer.json b/misc/typo3/phar-stream-wrapper/composer.json new file mode 100644 index 00000000000..d308f8c8741 --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/composer.json @@ -0,0 +1,24 @@ +{ + "name": "typo3/phar-stream-wrapper", + "description": "Interceptors for PHP's native phar:// stream handling", + "type": "library", + "license": "MIT", + "homepage": "https://typo3.org/", + "keywords": ["php", "phar", "stream-wrapper", "security"], + "require": { + "php": "^5.3.3|^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36" + }, + "autoload": { + "psr-4": { + "TYPO3\\PharStreamWrapper\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "TYPO3\\PharStreamWrapper\\Tests\\": "tests/" + } + } +} diff --git a/misc/typo3/phar-stream-wrapper/src/Assertable.php b/misc/typo3/phar-stream-wrapper/src/Assertable.php new file mode 100644 index 00000000000..a21b1da2ab9 --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/src/Assertable.php @@ -0,0 +1,22 @@ +assertCommands($commands); + $commands = $commands ?: $this->availableCommands; + + $target = clone $this; + foreach ($commands as $command) { + $target->assertions[$command] = $assertable; + } + return $target; + } + + /** + * @param string $path + * @param string $command + * @return bool + */ + public function assert($path, $command) + { + $this->assertCommand($command); + $this->assertAssertionCompleteness(); + + return $this->assertions[$command]->assert($path, $command); + } + + /** + * @param array $commands + */ + private function assertCommands(array $commands) + { + $unknownCommands = array_diff($commands, $this->availableCommands); + if (empty($unknownCommands)) { + return; + } + throw new \LogicException( + sprintf( + 'Unknown commands: %s', + implode(', ', $unknownCommands) + ), + 1535189881 + ); + } + + private function assertCommand($command) + { + if (in_array($command, $this->availableCommands, true)) { + return; + } + throw new \LogicException( + sprintf( + 'Unknown command "%s"', + $command + ), + 1535189882 + ); + } + + private function assertAssertionCompleteness() + { + $undefinedAssertions = array_diff( + $this->availableCommands, + array_keys($this->assertions) + ); + if (empty($undefinedAssertions)) { + return; + } + throw new \LogicException( + sprintf( + 'Missing assertions for commands: %s', + implode(', ', $undefinedAssertions) + ), + 1535189883 + ); + } +} diff --git a/misc/typo3/phar-stream-wrapper/src/Exception.php b/misc/typo3/phar-stream-wrapper/src/Exception.php new file mode 100644 index 00000000000..690121a999a --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/src/Exception.php @@ -0,0 +1,16 @@ += 1) { + // Rremove this and previous element + array_splice($pathParts, $partCount - 1, 2); + $partCount -= 2; + $pathPartsLength -= 2; + } elseif ($absolutePathPrefix) { + // can't go higher than root dir + // simply remove this part and continue + array_splice($pathParts, $partCount, 1); + $partCount--; + $pathPartsLength--; + } + } + } + + return $absolutePathPrefix . implode('/', $pathParts); + } + + /** + * Checks if the $path is absolute or relative (detecting either '/' or + * 'x:/' as first part of string) and returns TRUE if so. + * + * @param string $path File path to evaluate + * @return bool + */ + private static function isAbsolutePath($path) + { + // Path starting with a / is always absolute, on every system + // On Windows also a path starting with a drive letter is absolute: X:/ + return (isset($path[0]) ? $path[0] : null) === '/' + || static::isWindows() && ( + strpos($path, ':/') === 1 + || strpos($path, ':\\') === 1 + ); + } + + /** + * @return bool + */ + private static function isWindows() + { + return stripos(PHP_OS, 'WIN') === 0; + } +} \ No newline at end of file diff --git a/misc/typo3/phar-stream-wrapper/src/Interceptor/PharExtensionInterceptor.php b/misc/typo3/phar-stream-wrapper/src/Interceptor/PharExtensionInterceptor.php new file mode 100644 index 00000000000..db500afc8a7 --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/src/Interceptor/PharExtensionInterceptor.php @@ -0,0 +1,55 @@ +baseFileContainsPharExtension($path)) { + return true; + } + throw new Exception( + sprintf( + 'Unexpected file extension in "%s"', + $path + ), + 1535198703 + ); + } + + /** + * @param string $path + * @return bool + */ + private function baseFileContainsPharExtension($path) + { + $baseFile = Helper::determineBaseFile($path); + if ($baseFile === null) { + return false; + } + $fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION); + return strtolower($fileExtension) === 'phar'; + } +} diff --git a/misc/typo3/phar-stream-wrapper/src/Manager.php b/misc/typo3/phar-stream-wrapper/src/Manager.php new file mode 100644 index 00000000000..1eb9735d986 --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/src/Manager.php @@ -0,0 +1,85 @@ +behavior = $behaviour; + } + + /** + * @param string $path + * @param string $command + * @return bool + */ + public function assert($path, $command) + { + return $this->behavior->assert($path, $command); + } +} diff --git a/misc/typo3/phar-stream-wrapper/src/PharStreamWrapper.php b/misc/typo3/phar-stream-wrapper/src/PharStreamWrapper.php new file mode 100644 index 00000000000..5a924e4ccdf --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/src/PharStreamWrapper.php @@ -0,0 +1,477 @@ +internalResource)) { + return false; + } + + $this->invokeInternalStreamWrapper( + 'closedir', + $this->internalResource + ); + return !is_resource($this->internalResource); + } + + /** + * @param string $path + * @param int $options + * @return bool + */ + public function dir_opendir($path, $options) + { + $this->assert($path, Behavior::COMMAND_DIR_OPENDIR); + $this->internalResource = $this->invokeInternalStreamWrapper( + 'opendir', + $path, + $this->context + ); + return is_resource($this->internalResource); + } + + /** + * @return string|false + */ + public function dir_readdir() + { + return $this->invokeInternalStreamWrapper( + 'readdir', + $this->internalResource + ); + } + + /** + * @return bool + */ + public function dir_rewinddir() + { + if (!is_resource($this->internalResource)) { + return false; + } + + $this->invokeInternalStreamWrapper( + 'rewinddir', + $this->internalResource + ); + return is_resource($this->internalResource); + } + + /** + * @param string $path + * @param int $mode + * @param int $options + * @return bool + */ + public function mkdir($path, $mode, $options) + { + $this->assert($path, Behavior::COMMAND_MKDIR); + return $this->invokeInternalStreamWrapper( + 'mkdir', + $path, + $mode, + (bool) ($options & STREAM_MKDIR_RECURSIVE), + $this->context + ); + } + + /** + * @param string $path_from + * @param string $path_to + * @return bool + */ + public function rename($path_from, $path_to) + { + $this->assert($path_from, Behavior::COMMAND_RENAME); + $this->assert($path_to, Behavior::COMMAND_RENAME); + return $this->invokeInternalStreamWrapper( + 'rename', + $path_from, + $path_to, + $this->context + ); + } + + /** + * @param string $path + * @param int $options + * @return bool + */ + public function rmdir($path, $options) + { + $this->assert($path, Behavior::COMMAND_RMDIR); + return $this->invokeInternalStreamWrapper( + 'rmdir', + $path, + $this->context + ); + } + + /** + * @param int $cast_as + */ + public function stream_cast($cast_as) + { + throw new Exception( + 'Method stream_select() cannot be used', + 1530103999 + ); + } + + public function stream_close() + { + $this->invokeInternalStreamWrapper( + 'fclose', + $this->internalResource + ); + } + + /** + * @return bool + */ + public function stream_eof() + { + return $this->invokeInternalStreamWrapper( + 'feof', + $this->internalResource + ); + } + + /** + * @return bool + */ + public function stream_flush() + { + return $this->invokeInternalStreamWrapper( + 'fflush', + $this->internalResource + ); + } + + /** + * @param int $operation + * @return bool + */ + public function stream_lock($operation) + { + return $this->invokeInternalStreamWrapper( + 'flock', + $this->internalResource, + $operation + ); + } + + /** + * @param string $path + * @param int $option + * @param string|int $value + * @return bool + */ + public function stream_metadata($path, $option, $value) + { + $this->assert($path, Behavior::COMMAND_STEAM_METADATA); + if ($option === STREAM_META_TOUCH) { + return call_user_func_array( + array($this, 'invokeInternalStreamWrapper'), + array_merge(array('touch', $path), (array) $value) + ); + } + if ($option === STREAM_META_OWNER_NAME || $option === STREAM_META_OWNER) { + return $this->invokeInternalStreamWrapper( + 'chown', + $path, + $value + ); + } + if ($option === STREAM_META_GROUP_NAME || $option === STREAM_META_GROUP) { + return $this->invokeInternalStreamWrapper( + 'chgrp', + $path, + $value + ); + } + if ($option === STREAM_META_ACCESS) { + return $this->invokeInternalStreamWrapper( + 'chmod', + $path, + $value + ); + } + return false; + } + + /** + * @param string $path + * @param string $mode + * @param int $options + * @param string|null $opened_path + * @return bool + */ + public function stream_open( + $path, + $mode, + $options, + &$opened_path = null + ) { + $this->assert($path, Behavior::COMMAND_STREAM_OPEN); + $arguments = array($path, $mode, (bool) ($options & STREAM_USE_PATH)); + // only add stream context for non include/require calls + if (!($options & static::STREAM_OPEN_FOR_INCLUDE)) { + $arguments[] = $this->context; + // work around https://bugs.php.net/bug.php?id=66569 + // for including files from Phar stream with OPcache enabled + } else { + Helper::resetOpCache(); + } + $this->internalResource = call_user_func_array( + array($this, 'invokeInternalStreamWrapper'), + array_merge(array('fopen'), $arguments) + ); + if (!is_resource($this->internalResource)) { + return false; + } + if ($opened_path !== null) { + $metaData = stream_get_meta_data($this->internalResource); + $opened_path = $metaData['uri']; + } + return true; + } + + /** + * @param int $count + * @return string + */ + public function stream_read($count) + { + return $this->invokeInternalStreamWrapper( + 'fread', + $this->internalResource, + $count + ); + } + + /** + * @param int $offset + * @param int $whence + * @return bool + */ + public function stream_seek($offset, $whence = SEEK_SET) + { + return $this->invokeInternalStreamWrapper( + 'fseek', + $this->internalResource, + $offset, + $whence + ) !== -1; + } + + /** + * @param int $option + * @param int $arg1 + * @param int $arg2 + * @return bool + */ + public function stream_set_option($option, $arg1, $arg2) + { + if ($option === STREAM_OPTION_BLOCKING) { + return $this->invokeInternalStreamWrapper( + 'stream_set_blocking', + $this->internalResource, + $arg1 + ); + } + if ($option === STREAM_OPTION_READ_TIMEOUT) { + return $this->invokeInternalStreamWrapper( + 'stream_set_timeout', + $this->internalResource, + $arg1, + $arg2 + ); + } + if ($option === STREAM_OPTION_WRITE_BUFFER) { + return $this->invokeInternalStreamWrapper( + 'stream_set_write_buffer', + $this->internalResource, + $arg2 + ) === 0; + } + return false; + } + + /** + * @return array + */ + public function stream_stat() + { + return $this->invokeInternalStreamWrapper( + 'fstat', + $this->internalResource + ); + } + + /** + * @return int + */ + public function stream_tell() + { + return $this->invokeInternalStreamWrapper( + 'ftell', + $this->internalResource + ); + } + + /** + * @param int $new_size + * @return bool + */ + public function stream_truncate($new_size) + { + return $this->invokeInternalStreamWrapper( + 'ftruncate', + $this->internalResource, + $new_size + ); + } + + /** + * @param string $data + * @return int + */ + public function stream_write($data) + { + return $this->invokeInternalStreamWrapper( + 'fwrite', + $this->internalResource, + $data + ); + } + + /** + * @param string $path + * @return bool + */ + public function unlink($path) + { + $this->assert($path, Behavior::COMMAND_UNLINK); + return $this->invokeInternalStreamWrapper( + 'unlink', + $path, + $this->context + ); + } + + /** + * @param string $path + * @param int $flags + * @return array|false + */ + public function url_stat($path, $flags) + { + $this->assert($path, Behavior::COMMAND_URL_STAT); + $functionName = $flags & STREAM_URL_STAT_QUIET ? '@stat' : 'stat'; + return $this->invokeInternalStreamWrapper($functionName, $path); + } + + /** + * @param string $path + * @param string $command + */ + protected function assert($path, $command) + { + if ($this->resolveAssertable()->assert($path, $command) === true) { + return; + } + + throw new Exception( + sprintf( + 'Denied invocation of "%s" for command "%s"', + $path, + $command + ), + 1535189880 + ); + } + + /** + * @return Assertable + */ + protected function resolveAssertable() + { + return Manager::instance(); + } + + /** + * Invokes commands on the native PHP Phar stream wrapper. + * + * @param string $functionName + * @param mixed ...$arguments + * @return mixed + */ + private function invokeInternalStreamWrapper($functionName) + { + $arguments = func_get_args(); + array_shift($arguments); + $silentExecution = $functionName{0} === '@'; + $functionName = ltrim($functionName, '@'); + $this->restoreInternalSteamWrapper(); + + try { + if ($silentExecution) { + $result = @call_user_func_array($functionName, $arguments); + } else { + $result = call_user_func_array($functionName, $arguments); + } + } catch (\Exception $exception) { + $this->registerStreamWrapper(); + throw $exception; + } catch (\Throwable $throwable) { + $this->registerStreamWrapper(); + throw $throwable; + } + + $this->registerStreamWrapper(); + return $result; + } + + private function restoreInternalSteamWrapper() + { + stream_wrapper_restore('phar'); + } + + private function registerStreamWrapper() + { + stream_wrapper_unregister('phar'); + stream_wrapper_register('phar', get_class($this)); + } +} diff --git a/modules/system/system.module b/modules/system/system.module index 6f8561ebacb..83663c09695 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '6.47'); +define('VERSION', '6.48'); /** * Core API compatibility. From 0e725fdde176bb3bfbb46f8fe9f9fddb753ee322 Mon Sep 17 00:00:00 2001 From: David Snopek Date: Mon, 18 Feb 2019 11:06:00 -0600 Subject: [PATCH 3/5] Apply the changes from D6LTS 6.49 --- .../drupal-security/PharExtensionInterceptor.php | 12 +++++++++--- modules/system/system.module | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/misc/typo3/drupal-security/PharExtensionInterceptor.php b/misc/typo3/drupal-security/PharExtensionInterceptor.php index a77e9f84c26..2e1a0cbc8bb 100644 --- a/misc/typo3/drupal-security/PharExtensionInterceptor.php +++ b/misc/typo3/drupal-security/PharExtensionInterceptor.php @@ -22,7 +22,6 @@ class PharExtensionInterceptor implements Assertable { * * @param string $path * The path of the phar file to check. - * * @param string $command * The command being carried out. * @@ -46,6 +45,8 @@ public function assert($path, $command) { } /** + * Determines if a path has a .phar extension or invoked execution. + * * @param string $path * The path of the phar file to check. * @@ -62,8 +63,13 @@ private function baseFileContainsPharExtension($path) { // not not have .phar extension then this should be allowed. For // example, some CLI tools recommend removing the extension. $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - $caller = array_pop($backtrace); - if (isset($caller['file']) && $baseFile === $caller['file']) { + // Find the last entry in the backtrace containing a 'file' key as + // sometimes the last caller is executed outside the scope of a file. For + // example, this occurs with shutdown functions. + do { + $caller = array_pop($backtrace); + } while (empty($caller['file']) && !empty($backtrace)); + if (isset($caller['file']) && $baseFile === Helper::determineBaseFile($caller['file'])) { return TRUE; } $fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION); diff --git a/modules/system/system.module b/modules/system/system.module index 83663c09695..6a21c1de22e 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '6.48'); +define('VERSION', '6.49'); /** * Core API compatibility. From 76b558da999432d8da1e744a4255bb3bfe549435 Mon Sep 17 00:00:00 2001 From: David Snopek Date: Wed, 8 May 2019 15:11:29 -0500 Subject: [PATCH 4/5] Apply the changes from D6LTS 6.52 --- CHANGELOG.txt | 60 ++- includes/common.inc | 12 + includes/database.inc | 344 +++++++++++++++++- includes/database.mysql.inc | 7 + includes/database.mysqli.inc | 7 + includes/file.phar.inc | 14 + misc/brumann/polyfill-unserialize/.gitignore | 4 + misc/brumann/polyfill-unserialize/.travis.yml | 20 + misc/brumann/polyfill-unserialize/LICENSE | 21 ++ misc/brumann/polyfill-unserialize/README.md | 61 ++++ .../polyfill-unserialize/composer.json | 26 ++ .../polyfill-unserialize/phpunit.xml.dist | 25 ++ .../polyfill-unserialize/src/Unserialize.php | 58 +++ misc/jquery-extend-3.4.0.js | 174 +++++++++ misc/typo3/phar-stream-wrapper/.gitignore | 3 + misc/typo3/phar-stream-wrapper/README.md | 69 +++- misc/typo3/phar-stream-wrapper/composer.json | 6 +- .../phar-stream-wrapper/src/Collectable.php | 37 ++ misc/typo3/phar-stream-wrapper/src/Helper.php | 20 +- .../Interceptor/ConjunctionInterceptor.php | 88 +++++ .../Interceptor/PharExtensionInterceptor.php | 8 +- .../Interceptor/PharMetaDataInterceptor.php | 73 ++++ .../typo3/phar-stream-wrapper/src/Manager.php | 62 +++- .../src/Phar/Container.php | 59 +++ .../src/Phar/DeserializationException.php | 18 + .../phar-stream-wrapper/src/Phar/Manifest.php | 176 +++++++++ .../phar-stream-wrapper/src/Phar/Reader.php | 220 +++++++++++ .../src/Phar/ReaderException.php | 18 + .../phar-stream-wrapper/src/Phar/Stub.php | 65 ++++ .../src/PharStreamWrapper.php | 38 +- .../phar-stream-wrapper/src/Resolvable.php | 24 ++ .../src/Resolver/PharInvocation.php | 125 +++++++ .../src/Resolver/PharInvocationCollection.php | 156 ++++++++ .../src/Resolver/PharInvocationResolver.php | 241 ++++++++++++ modules/system/system.module | 2 +- modules/user/user.module | 2 +- 36 files changed, 2297 insertions(+), 46 deletions(-) create mode 100644 misc/brumann/polyfill-unserialize/.gitignore create mode 100644 misc/brumann/polyfill-unserialize/.travis.yml create mode 100644 misc/brumann/polyfill-unserialize/LICENSE create mode 100644 misc/brumann/polyfill-unserialize/README.md create mode 100644 misc/brumann/polyfill-unserialize/composer.json create mode 100644 misc/brumann/polyfill-unserialize/phpunit.xml.dist create mode 100644 misc/brumann/polyfill-unserialize/src/Unserialize.php create mode 100644 misc/jquery-extend-3.4.0.js create mode 100644 misc/typo3/phar-stream-wrapper/.gitignore create mode 100644 misc/typo3/phar-stream-wrapper/src/Collectable.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Interceptor/ConjunctionInterceptor.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Interceptor/PharMetaDataInterceptor.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Phar/Container.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Phar/DeserializationException.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Phar/Manifest.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Phar/Reader.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Phar/ReaderException.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Phar/Stub.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Resolvable.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Resolver/PharInvocation.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Resolver/PharInvocationCollection.php create mode 100644 misc/typo3/phar-stream-wrapper/src/Resolver/PharInvocationResolver.php 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/common.inc b/includes/common.inc index de895897edd..78d048ec38d 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -2243,6 +2243,7 @@ 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/drupal.js' => array('cache' => TRUE, 'defer' => FALSE, 'preprocess' => TRUE), ), 'module' => array(), @@ -3583,6 +3584,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.phar.inc b/includes/file.phar.inc index 0e198901c87..f3b24d332f8 100644 --- a/includes/file.phar.inc +++ b/includes/file.phar.inc @@ -18,7 +18,21 @@ function file_register_phar_wrapper() { include_once $directory . '/Helper.php'; include_once $directory . '/Manager.php'; include_once $directory . '/PharStreamWrapper.php'; + include_once $directory . '/Collectable.php'; + include_once $directory . '/Interceptor/ConjunctionInterceptor.php'; + include_once $directory . '/Interceptor/PharMetaDataInterceptor.php'; + include_once $directory . '/Phar/Container.php'; + include_once $directory . '/Phar/DeserializationException.php'; + include_once $directory . '/Phar/Manifest.php'; + include_once $directory . '/Phar/Reader.php'; + include_once $directory . '/Phar/ReaderException.php'; + include_once $directory . '/Phar/Stub.php'; + include_once $directory . '/Resolvable.php'; + include_once $directory . '/Resolver/PharInvocation.php'; + include_once $directory . '/Resolver/PharInvocationCollection.php'; + include_once $directory . '/Resolver/PharInvocationResolver.php'; include_once './misc/typo3/drupal-security/PharExtensionInterceptor.php'; + include_once './misc/brumann/polyfill-unserialize/src/Unserialize.php'; // Set up a stream wrapper to handle insecurities due to PHP's built-in // phar stream wrapper. 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/typo3/phar-stream-wrapper/.gitignore b/misc/typo3/phar-stream-wrapper/.gitignore new file mode 100644 index 00000000000..157ff0c5989 --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/.gitignore @@ -0,0 +1,3 @@ +.idea +vendor/ +composer.lock diff --git a/misc/typo3/phar-stream-wrapper/README.md b/misc/typo3/phar-stream-wrapper/README.md index b632784bdda..179bb6fd774 100644 --- a/misc/typo3/phar-stream-wrapper/README.md +++ b/misc/typo3/phar-stream-wrapper/README.md @@ -63,7 +63,7 @@ adjusted to according requirements. ``` $behavior = new \TYPO3\PharStreamWrapper\Behavior(); -Manager::initialize( +\TYPO3\PharStreamWrapper\Manager::initialize( $behavior->withAssertion(new PharExtensionInterceptor()) ); @@ -90,7 +90,7 @@ if (in_array('phar', stream_get_wrappers())) { + `COMMAND_UNLINK` + `COMMAND_URL_STAT` -## Interceptor +## Interceptors The following interceptor is shipped with the package and ready to use in order to block any Phar invocation of files not having a `.phar` suffix. Besides that @@ -137,9 +137,72 @@ class PharExtensionInterceptor implements Assertable } ``` +### ConjunctionInterceptor + +This interceptor combines multiple interceptors implementing `Assertable`. +It succeeds when all nested interceptors succeed as well (logical `AND`). + +``` +$behavior = new \TYPO3\PharStreamWrapper\Behavior(); +\TYPO3\PharStreamWrapper\Manager::initialize( + $behavior->withAssertion(new ConjunctionInterceptor(array( + new PharExtensionInterceptor(), + new PharMetaDataInterceptor() + ))) +); +``` + +### PharExtensionInterceptor + +This (basic) interceptor just checks whether the invoked Phar archive has +an according `.phar` file extension. Resolving symbolic links as well as +Phar internal alias resolving are considered as well. + +``` +$behavior = new \TYPO3\PharStreamWrapper\Behavior(); +\TYPO3\PharStreamWrapper\Manager::initialize( + $behavior->withAssertion(new PharExtensionInterceptor()) +); +``` + +### PharMetaDataInterceptor + +This interceptor is actually checking serialized Phar meta-data against +PHP objects and would consider a Phar archive malicious in case not only +scalar values are found. A custom low-level `Phar\Reader` is used in order to +avoid using PHP's `Phar` object which would trigger the initial vulnerability. + +``` +$behavior = new \TYPO3\PharStreamWrapper\Behavior(); +\TYPO3\PharStreamWrapper\Manager::initialize( + $behavior->withAssertion(new PharMetaDataInterceptor()) +); +``` + +## Reader + +* `Phar\Reader::__construct(string $fileName)`: Creates low-level reader for Phar archive +* `Phar\Reader::resolveContainer(): Phar\Container`: Resolves model representing Phar archive +* `Phar\Container::getStub(): Phar\Stub`: Resolves (plain PHP) stub section of Phar archive +* `Phar\Container::getManifest(): Phar\Manifest`: Resolves parsed Phar archive manifest as + documented at http://php.net/manual/en/phar.fileformat.manifestfile.php +* `Phar\Stub::getMappedAlias(): string`: Resolves internal Phar archive alias defined in stub + using `Phar::mapPhar('alias.phar')` - actually the plain PHP source is analyzed here +* `Phar\Manifest::getAlias(): string` - Resolves internal Phar archive alias defined in manifest + using `Phar::setAlias('alias.phar')` +* `Phar\Manifest::getMetaData(): string`: Resolves serialized Phar archive meta-data +* `Phar\Manifest::deserializeMetaData(): mixed`: Resolves deserialized Phar archive meta-data + containing only scalar values - in case an object is determined, an according + `Phar\DeserializationException` will be thrown + +``` +$reader = new Phar\Reader('example.phar'); +var_dump($reader->resolveContainer()->getManifest()->deserializeMetaData()); +``` + ## Helper -* `Helper::determineBaseFile(string $path)`: Determines base file that can be +* `Helper::determineBaseFile(string $path): string`: Determines base file that can be accessed using the regular file system. For instance the following path `phar:///home/user/bundle.phar/content.txt` would be resolved to `/home/user/bundle.phar`. diff --git a/misc/typo3/phar-stream-wrapper/composer.json b/misc/typo3/phar-stream-wrapper/composer.json index d308f8c8741..8c224118750 100644 --- a/misc/typo3/phar-stream-wrapper/composer.json +++ b/misc/typo3/phar-stream-wrapper/composer.json @@ -6,9 +6,13 @@ "homepage": "https://typo3.org/", "keywords": ["php", "phar", "stream-wrapper", "security"], "require": { - "php": "^5.3.3|^7.0" + "php": "^5.3.3|^7.0", + "ext-fileinfo": "*", + "ext-json": "*", + "brumann/polyfill-unserialize": "^1.0" }, "require-dev": { + "ext-xdebug": "*", "phpunit/phpunit": "^4.8.36" }, "autoload": { diff --git a/misc/typo3/phar-stream-wrapper/src/Collectable.php b/misc/typo3/phar-stream-wrapper/src/Collectable.php new file mode 100644 index 00000000000..4694dc946e9 --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/src/Collectable.php @@ -0,0 +1,37 @@ +assertAssertions($assertions); + $this->assertions = $assertions; + } + + /** + * Executes assertions based on all contained assertions. + * + * @param string $path + * @param string $command + * @return bool + * @throws Exception + */ + public function assert($path, $command) + { + if ($this->invokeAssertions($path, $command)) { + return true; + } + throw new Exception( + sprintf( + 'Assertion failed in "%s"', + $path + ), + 1539625084 + ); + } + + /** + * @param Assertable[] $assertions + */ + private function assertAssertions(array $assertions) + { + foreach ($assertions as $assertion) { + if (!$assertion instanceof Assertable) { + throw new \InvalidArgumentException( + sprintf( + 'Instance %s must implement Assertable', + get_class($assertion) + ), + 1539624719 + ); + } + } + } + + /** + * @param string $path + * @param string $command + * @return bool + */ + private function invokeAssertions($path, $command) + { + try { + foreach ($this->assertions as $assertion) { + if (!$assertion->assert($path, $command)) { + return false; + } + } + } catch (Exception $exception) { + return false; + } + return true; + } +} diff --git a/misc/typo3/phar-stream-wrapper/src/Interceptor/PharExtensionInterceptor.php b/misc/typo3/phar-stream-wrapper/src/Interceptor/PharExtensionInterceptor.php index db500afc8a7..6e7aeedcbe7 100644 --- a/misc/typo3/phar-stream-wrapper/src/Interceptor/PharExtensionInterceptor.php +++ b/misc/typo3/phar-stream-wrapper/src/Interceptor/PharExtensionInterceptor.php @@ -12,8 +12,8 @@ */ use TYPO3\PharStreamWrapper\Assertable; -use TYPO3\PharStreamWrapper\Helper; use TYPO3\PharStreamWrapper\Exception; +use TYPO3\PharStreamWrapper\Manager; class PharExtensionInterceptor implements Assertable { @@ -45,11 +45,11 @@ public function assert($path, $command) */ private function baseFileContainsPharExtension($path) { - $baseFile = Helper::determineBaseFile($path); - if ($baseFile === null) { + $invocation = Manager::instance()->resolve($path); + if ($invocation === null) { return false; } - $fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION); + $fileExtension = pathinfo($invocation->getBaseName(), PATHINFO_EXTENSION); return strtolower($fileExtension) === 'phar'; } } diff --git a/misc/typo3/phar-stream-wrapper/src/Interceptor/PharMetaDataInterceptor.php b/misc/typo3/phar-stream-wrapper/src/Interceptor/PharMetaDataInterceptor.php new file mode 100644 index 00000000000..e981dc6a69e --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/src/Interceptor/PharMetaDataInterceptor.php @@ -0,0 +1,73 @@ +baseFileDoesNotHaveMetaDataIssues($path)) { + return true; + } + throw new Exception( + sprintf( + 'Problematic meta-data in "%s"', + $path + ), + 1539632368 + ); + } + + /** + * @param string $path + * @return bool + */ + private function baseFileDoesNotHaveMetaDataIssues($path) + { + $invocation = Manager::instance()->resolve($path); + if ($invocation === null) { + return false; + } + // directly return in case invocation was checked before + if ($invocation->getVariable(__CLASS__) === true) { + return true; + } + // otherwise analyze meta-data + try { + $reader = new Reader($invocation->getBaseName()); + $reader->resolveContainer()->getManifest()->deserializeMetaData(); + $invocation->setVariable(__CLASS__, true); + } catch (DeserializationException $exception) { + return false; + } + return true; + } +} diff --git a/misc/typo3/phar-stream-wrapper/src/Manager.php b/misc/typo3/phar-stream-wrapper/src/Manager.php index 1eb9735d986..f938ad98541 100644 --- a/misc/typo3/phar-stream-wrapper/src/Manager.php +++ b/misc/typo3/phar-stream-wrapper/src/Manager.php @@ -11,7 +11,11 @@ * The TYPO3 project - inspiring people to share! */ -class Manager implements Assertable +use TYPO3\PharStreamWrapper\Resolver\PharInvocation; +use TYPO3\PharStreamWrapper\Resolver\PharInvocationCollection; +use TYPO3\PharStreamWrapper\Resolver\PharInvocationResolver; + +class Manager { /** * @var self @@ -23,14 +27,29 @@ class Manager implements Assertable */ private $behavior; + /** + * @var Resolvable + */ + private $resolver; + + /** + * @var Collectable + */ + private $collection; + /** * @param Behavior $behaviour + * @param Resolvable $resolver + * @param Collectable $collection * @return self */ - public static function initialize(Behavior $behaviour) - { + public static function initialize( + Behavior $behaviour, + Resolvable $resolver = null, + Collectable $collection = null + ) { if (self::$instance === null) { - self::$instance = new self($behaviour); + self::$instance = new self($behaviour, $resolver, $collection); return self::$instance; } throw new \LogicException( @@ -67,9 +86,22 @@ public static function destroy() /** * @param Behavior $behaviour + * @param Resolvable $resolver + * @param Collectable $collection */ - private function __construct(Behavior $behaviour) - { + private function __construct( + Behavior $behaviour, + Resolvable $resolver = null, + Collectable $collection = null + ) { + if ($collection === null) { + $collection = new PharInvocationCollection(); + } + if ($resolver === null) { + $resolver = new PharInvocationResolver(); + } + $this->collection = $collection; + $this->resolver = $resolver; $this->behavior = $behaviour; } @@ -82,4 +114,22 @@ public function assert($path, $command) { return $this->behavior->assert($path, $command); } + + /** + * @param string $path + * @param null|int $flags + * @return null|PharInvocation + */ + public function resolve($path, $flags = null) + { + return $this->resolver->resolve($path, $flags); + } + + /** + * @return Collectable + */ + public function getCollection() + { + return $this->collection; + } } diff --git a/misc/typo3/phar-stream-wrapper/src/Phar/Container.php b/misc/typo3/phar-stream-wrapper/src/Phar/Container.php new file mode 100644 index 00000000000..f02387d7388 --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/src/Phar/Container.php @@ -0,0 +1,59 @@ +stub = $stub; + $this->manifest = $manifest; + } + + /** + * @return Stub + */ + public function getStub() + { + return $this->stub; + } + + /** + * @return Manifest + */ + public function getManifest() + { + return $this->manifest; + } + + /** + * @return string + */ + public function getAlias() + { + return $this->manifest->getAlias() ?: $this->stub->getMappedAlias(); + } +} diff --git a/misc/typo3/phar-stream-wrapper/src/Phar/DeserializationException.php b/misc/typo3/phar-stream-wrapper/src/Phar/DeserializationException.php new file mode 100644 index 00000000000..5a675d34fe9 --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/src/Phar/DeserializationException.php @@ -0,0 +1,18 @@ +manifestLength = Reader::resolveFourByteLittleEndian($content, 0); + $target->amountOfFiles = Reader::resolveFourByteLittleEndian($content, 4); + $target->flags = Reader::resolveFourByteLittleEndian($content, 10); + $target->aliasLength = Reader::resolveFourByteLittleEndian($content, 14); + $target->alias = substr($content, 18, $target->aliasLength); + $target->metaDataLength = Reader::resolveFourByteLittleEndian($content, 18 + $target->aliasLength); + $target->metaData = substr($content, 22 + $target->aliasLength, $target->metaDataLength); + + $apiVersionNibbles = Reader::resolveTwoByteBigEndian($content, 8); + $target->apiVersion = implode('.', array( + ($apiVersionNibbles & 0xf000) >> 12, + ($apiVersionNibbles & 0x0f00) >> 8, + ($apiVersionNibbles & 0x00f0) >> 4, + )); + + return $target; + } + + /** + * @var int + */ + private $manifestLength; + + /** + * @var int + */ + private $amountOfFiles; + + /** + * @var string + */ + private $apiVersion; + + /** + * @var int + */ + private $flags; + + /** + * @var int + */ + private $aliasLength; + + /** + * @var string + */ + private $alias; + + /** + * @var int + */ + private $metaDataLength; + + /** + * @var string + */ + private $metaData; + + /** + * Avoid direct instantiation. + */ + private function __construct() + { + } + + /** + * @return int + */ + public function getManifestLength() + { + return $this->manifestLength; + } + + /** + * @return int + */ + public function getAmountOfFiles() + { + return $this->amountOfFiles; + } + + /** + * @return string + */ + public function getApiVersion() + { + return $this->apiVersion; + } + + /** + * @return int + */ + public function getFlags() + { + return $this->flags; + } + + /** + * @return int + */ + public function getAliasLength() + { + return $this->aliasLength; + } + + /** + * @return string + */ + public function getAlias() + { + return $this->alias; + } + + /** + * @return int + */ + public function getMetaDataLength() + { + return $this->metaDataLength; + } + + /** + * @return string + */ + public function getMetaData() + { + return $this->metaData; + } + + /** + * @return mixed|null + */ + public function deserializeMetaData() + { + if (empty($this->metaData)) { + return null; + } + + $result = Unserialize::unserialize($this->metaData, array('allowed_classes' => false)); + + $serialized = json_encode($result); + if (strpos($serialized, '__PHP_Incomplete_Class_Name') !== false) { + throw new DeserializationException( + 'Meta-data contains serialized object', + 1539623382 + ); + } + + return $result; + } +} diff --git a/misc/typo3/phar-stream-wrapper/src/Phar/Reader.php b/misc/typo3/phar-stream-wrapper/src/Phar/Reader.php new file mode 100644 index 00000000000..32e516be3a8 --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/src/Phar/Reader.php @@ -0,0 +1,220 @@ +fileName = $fileName; + $this->fileType = $this->determineFileType(); + } + + /** + * @return Container + */ + public function resolveContainer() + { + $data = $this->extractData($this->resolveStream() . $this->fileName); + + if ($data['stubContent'] === null) { + throw new ReaderException( + 'Cannot resolve stub', + 1547807881 + ); + } + if ($data['manifestContent'] === null || $data['manifestLength'] === null) { + throw new ReaderException( + 'Cannot resolve manifest', + 1547807882 + ); + } + if (strlen($data['manifestContent']) < $data['manifestLength']) { + throw new ReaderException( + sprintf( + 'Exected manifest length %d, got %d', + strlen($data['manifestContent']), + $data['manifestLength'] + ), + 1547807883 + ); + } + + return new Container( + Stub::fromContent($data['stubContent']), + Manifest::fromContent($data['manifestContent']) + ); + } + + /** + * @param string $fileName e.g. '/path/file.phar' or 'compress.zlib:///path/file.phar' + * @return array + */ + private function extractData($fileName) + { + $stubContent = null; + $manifestContent = null; + $manifestLength = null; + + $resource = fopen($fileName, 'r'); + if (!is_resource($resource)) { + throw new ReaderException( + sprintf('Resource %s could not be opened', $fileName), + 1547902055 + ); + } + + while (!feof($resource)) { + $line = fgets($resource); + // stop reading file when manifest can be extracted + if ($manifestLength !== null && $manifestContent !== null && strlen($manifestContent) >= $manifestLength) { + break; + } + + $manifestPosition = strpos($line, '__HALT_COMPILER();'); + + // first line contains start of manifest + if ($stubContent === null && $manifestContent === null && $manifestPosition !== false) { + $stubContent = substr($line, 0, $manifestPosition - 1); + $manifestContent = preg_replace('#^.*__HALT_COMPILER\(\);(?>[ \n]\?>(?>\r\n|\n)?)?#', '', $line); + $manifestLength = $this->resolveManifestLength($manifestContent); + // line contains start of stub + } elseif ($stubContent === null) { + $stubContent = $line; + // line contains start of manifest + } elseif ($manifestContent === null && $manifestPosition !== false) { + $manifestContent = preg_replace('#^.*__HALT_COMPILER\(\);(?>[ \n]\?>(?>\r\n|\n)?)?#', '', $line); + $manifestLength = $this->resolveManifestLength($manifestContent); + // manifest has been started (thus is cannot be stub anymore), add content + } elseif ($manifestContent !== null) { + $manifestContent .= $line; + $manifestLength = $this->resolveManifestLength($manifestContent); + // stub has been started (thus cannot be manifest here, yet), add content + } elseif ($stubContent !== null) { + $stubContent .= $line; + } + } + fclose($resource); + + return array( + 'stubContent' => $stubContent, + 'manifestContent' => $manifestContent, + 'manifestLength' => $manifestLength, + ); + } + + /** + * Resolves stream in order to handle compressed Phar archives. + * + * @return string + */ + private function resolveStream() + { + if ($this->fileType === 'application/x-gzip') { + return 'compress.zlib://'; + } elseif ($this->fileType === 'application/x-bzip2') { + return 'compress.bzip2://'; + } + return ''; + } + + /** + * @return string + */ + private function determineFileType() + { + $fileInfo = new \finfo(); + return $fileInfo->file($this->fileName, FILEINFO_MIME_TYPE); + } + + /** + * @param string $content + * @return int|null + */ + private function resolveManifestLength($content) + { + if (strlen($content) < 4) { + return null; + } + return static::resolveFourByteLittleEndian($content, 0); + } + + /** + * @param string $content + * @param int $start + * @return int + */ + public static function resolveFourByteLittleEndian($content, $start) + { + $payload = substr($content, $start, 4); + if (!is_string($payload)) { + throw new ReaderException( + sprintf('Cannot resolve value at offset %d', $start), + 1539614260 + ); + } + + $value = unpack('V', $payload); + if (!isset($value[1])) { + throw new ReaderException( + sprintf('Cannot resolve value at offset %d', $start), + 1539614261 + ); + } + return $value[1]; + } + + /** + * @param string $content + * @param int $start + * @return int + */ + public static function resolveTwoByteBigEndian($content, $start) + { + $payload = substr($content, $start, 2); + if (!is_string($payload)) { + throw new ReaderException( + sprintf('Cannot resolve value at offset %d', $start), + 1539614263 + ); + } + + $value = unpack('n', $payload); + if (!isset($value[1])) { + throw new ReaderException( + sprintf('Cannot resolve value at offset %d', $start), + 1539614264 + ); + } + return $value[1]; + } +} diff --git a/misc/typo3/phar-stream-wrapper/src/Phar/ReaderException.php b/misc/typo3/phar-stream-wrapper/src/Phar/ReaderException.php new file mode 100644 index 00000000000..002afe158de --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/src/Phar/ReaderException.php @@ -0,0 +1,18 @@ +content = $content; + + if ( + stripos($content, 'Phar::mapPhar(') !== false + && preg_match('#Phar\:\:mapPhar\(([^)]+)\)#', $content, $matches) + ) { + // remove spaces, single & double quotes + // @todo `'my' . 'alias' . '.phar'` is not evaluated here + $target->mappedAlias = trim($matches[1], ' \'"'); + } + + return $target; + } + + /** + * @var string + */ + private $content; + + /** + * @var string + */ + private $mappedAlias = ''; + + /** + * @return string + */ + public function getContent() + { + return $this->content; + } + + /** + * @return string + */ + public function getMappedAlias() + { + return $this->mappedAlias; + } +} diff --git a/misc/typo3/phar-stream-wrapper/src/PharStreamWrapper.php b/misc/typo3/phar-stream-wrapper/src/PharStreamWrapper.php index 5a924e4ccdf..acd5656f47b 100644 --- a/misc/typo3/phar-stream-wrapper/src/PharStreamWrapper.php +++ b/misc/typo3/phar-stream-wrapper/src/PharStreamWrapper.php @@ -11,6 +11,8 @@ * The TYPO3 project - inspiring people to share! */ +use TYPO3\PharStreamWrapper\Resolver\PharInvocation; + class PharStreamWrapper { /** @@ -29,6 +31,11 @@ class PharStreamWrapper */ protected $internalResource; + /** + * @var PharInvocation + */ + protected $invocation; + /** * @return bool */ @@ -409,7 +416,8 @@ public function url_stat($path, $flags) */ protected function assert($path, $command) { - if ($this->resolveAssertable()->assert($path, $command) === true) { + if (Manager::instance()->assert($path, $command) === true) { + $this->collectInvocation($path); return; } @@ -424,7 +432,33 @@ protected function assert($path, $command) } /** - * @return Assertable + * @param string $path + */ + protected function collectInvocation($path) + { + if (isset($this->invocation)) { + return; + } + + $manager = Manager::instance(); + $this->invocation = $manager->resolve($path); + if ($this->invocation === null) { + throw new Exception( + 'Expected invocation could not be resolved', + 1556389591 + ); + } + // confirm, previous interceptor(s) validated invocation + $this->invocation->confirm(); + $collection = $manager->getCollection(); + if (!$collection->has($this->invocation)) { + $collection->collect($this->invocation); + } + } + + /** + * @return Manager|Assertable + * @deprecated Use Manager::instance() directly */ protected function resolveAssertable() { diff --git a/misc/typo3/phar-stream-wrapper/src/Resolvable.php b/misc/typo3/phar-stream-wrapper/src/Resolvable.php new file mode 100644 index 00000000000..5d5fdc63ee1 --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/src/Resolvable.php @@ -0,0 +1,24 @@ +baseName = $baseName; + $this->alias = $alias; + } + + /** + * @return string + */ + public function __toString() + { + return $this->baseName; + } + + /** + * @return string + */ + public function getBaseName() + { + return $this->baseName; + } + + /** + * @return null|string + */ + public function getAlias() + { + return $this->alias; + } + + /** + * @return bool + */ + public function isConfirmed() + { + return $this->confirmed; + } + + public function confirm() + { + $this->confirmed = true; + } + + /** + * @param string $name + * @return mixed|null + */ + public function getVariable($name) + { + if (!isset($this->variables[$name])) { + return null; + } + return $this->variables[$name]; + } + + /** + * @param string $name + * @param mixed $value + */ + public function setVariable($name, $value) + { + $this->variables[$name] = $value; + } + + /** + * @param PharInvocation $other + * @return bool + */ + public function equals(PharInvocation $other) + { + return $other->baseName === $this->baseName + && $other->alias === $this->alias; + } +} \ No newline at end of file diff --git a/misc/typo3/phar-stream-wrapper/src/Resolver/PharInvocationCollection.php b/misc/typo3/phar-stream-wrapper/src/Resolver/PharInvocationCollection.php new file mode 100644 index 00000000000..e445ff66e9c --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/src/Resolver/PharInvocationCollection.php @@ -0,0 +1,156 @@ +invocations, true); + } + + /** + * @param PharInvocation $invocation + * @param null|int $flags + * @return bool + */ + public function collect(PharInvocation $invocation, $flags = null) + { + if ($flags === null) { + $flags = static::UNIQUE_INVOCATION | static::DUPLICATE_ALIAS_WARNING; + } + if ($invocation->getBaseName() === '' + || $invocation->getAlias() === '' + || !$this->assertUniqueBaseName($invocation, $flags) + || !$this->assertUniqueInvocation($invocation, $flags) + ) { + return false; + } + if ($flags & static::DUPLICATE_ALIAS_WARNING) { + $this->triggerDuplicateAliasWarning($invocation); + } + + $this->invocations[] = $invocation; + return true; + } + + /** + * @param callable $callback + * @param bool $reverse + * @return null|PharInvocation + */ + public function findByCallback($callback, $reverse = false) + { + foreach ($this->getInvocations($reverse) as $invocation) { + if (call_user_func($callback, $invocation) === true) { + return $invocation; + } + } + return null; + } + + /** + * Asserts that base-name is unique. This disallows having multiple invocations for + * same base-name but having different alias names. + * + * @param PharInvocation $invocation + * @param int $flags + * @return bool + */ + private function assertUniqueBaseName(PharInvocation $invocation, $flags) + { + if (!($flags & static::UNIQUE_BASE_NAME)) { + return true; + } + return $this->findByCallback( + function (PharInvocation $candidate) use ($invocation) { + return $candidate->getBaseName() === $invocation->getBaseName(); + } + ) === null; + } + + /** + * Asserts that combination of base-name and alias is unique. This allows having multiple + * invocations for same base-name but having different alias names (for whatever reason). + * + * @param PharInvocation $invocation + * @param int $flags + * @return bool + */ + private function assertUniqueInvocation(PharInvocation $invocation, $flags) + { + if (!($flags & static::UNIQUE_INVOCATION)) { + return true; + } + return $this->findByCallback( + function (PharInvocation $candidate) use ($invocation) { + return $candidate->equals($invocation); + } + ) === null; + } + + /** + * Triggers warning for invocations with same alias and same confirmation state. + * + * @param PharInvocation $invocation + * @see \TYPO3\PharStreamWrapper\PharStreamWrapper::collectInvocation() + */ + private function triggerDuplicateAliasWarning(PharInvocation $invocation) + { + $sameAliasInvocation = $this->findByCallback( + function (PharInvocation $candidate) use ($invocation) { + return $candidate->isConfirmed() === $invocation->isConfirmed() + && $candidate->getAlias() === $invocation->getAlias(); + }, + true + ); + if ($sameAliasInvocation === null) { + return; + } + trigger_error( + sprintf( + 'Alias %s cannot be used by %s, already used by %s', + $invocation->getAlias(), + $invocation->getBaseName(), + $sameAliasInvocation->getBaseName() + ), + E_USER_WARNING + ); + } + + /** + * @param bool $reverse + * @return PharInvocation[] + */ + private function getInvocations($reverse = false) + { + if ($reverse) { + return array_reverse($this->invocations); + } + return $this->invocations; + } +} \ No newline at end of file diff --git a/misc/typo3/phar-stream-wrapper/src/Resolver/PharInvocationResolver.php b/misc/typo3/phar-stream-wrapper/src/Resolver/PharInvocationResolver.php new file mode 100644 index 00000000000..80b86d3db42 --- /dev/null +++ b/misc/typo3/phar-stream-wrapper/src/Resolver/PharInvocationResolver.php @@ -0,0 +1,241 @@ +findByAlias($path); + if ($invocation !== null) { + return $invocation; + } + } + + $baseName = $this->resolveBaseName($path, $flags); + if ($baseName === null) { + return null; + } + + if ($flags & static::RESOLVE_REALPATH) { + $baseName = $this->baseNames[$baseName]; + } + + return $this->retrieveInvocation($baseName, $flags); + } + + /** + * Retrieves PharInvocation, either existing in collection or created on demand + * with resolving a potential alias name used in the according Phar archive. + * + * @param string $baseName + * @param int $flags + * @return PharInvocation + */ + private function retrieveInvocation($baseName, $flags) + { + $invocation = $this->findByBaseName($baseName); + if ($invocation !== null) { + return $invocation; + } + + if ($flags & static::RESOLVE_ALIAS) { + $reader = new Reader($baseName); + $alias = $reader->resolveContainer()->getAlias(); + } else { + $alias = ''; + } + // add unconfirmed(!) new invocation to collection + $invocation = new PharInvocation($baseName, $alias); + Manager::instance()->getCollection()->collect($invocation); + return $invocation; + } + + /** + * @param string $path + * @param int $flags + * @return null|string + */ + private function resolveBaseName($path, $flags) + { + $baseName = $this->findInBaseNames($path); + if ($baseName !== null) { + return $baseName; + } + + $baseName = Helper::determineBaseFile($path); + if ($baseName !== null) { + $this->addBaseName($baseName); + return $baseName; + } + + $possibleAlias = $this->resolvePossibleAlias($path); + if (!($flags & static::RESOLVE_ALIAS) || $possibleAlias === null) { + return null; + } + + $trace = debug_backtrace(); + foreach ($trace as $item) { + if (!isset($item['function']) || !isset($item['args'][0]) + || !in_array($item['function'], $this->invocationFunctionNames, true)) { + continue; + } + $currentPath = $item['args'][0]; + if (Helper::hasPharPrefix($currentPath)) { + continue; + } + $currentBaseName = Helper::determineBaseFile($currentPath); + if ($currentBaseName === null) { + continue; + } + // ensure the possible alias name (how we have been called initially) matches + // the resolved alias name that was retrieved by the current possible base name + $reader = new Reader($currentBaseName); + $currentAlias = $reader->resolveContainer()->getAlias(); + if ($currentAlias !== $possibleAlias) { + continue; + } + $this->addBaseName($currentBaseName); + return $currentBaseName; + } + + return null; + } + + /** + * @param string $path + * @return null|string + */ + private function resolvePossibleAlias($path) + { + $normalizedPath = Helper::normalizePath($path); + return strstr($normalizedPath, '/', true) ?: null; + } + + /** + * @param string $baseName + * @return null|PharInvocation + */ + private function findByBaseName($baseName) + { + return Manager::instance()->getCollection()->findByCallback( + function (PharInvocation $candidate) use ($baseName) { + return $candidate->getBaseName() === $baseName; + }, + true + ); + } + + /** + * @param string $path + * @return null|string + */ + private function findInBaseNames($path) + { + // return directly if the resolved base name was submitted + if (in_array($path, $this->baseNames, true)) { + return $path; + } + + $parts = explode('/', Helper::normalizePath($path)); + + while (count($parts)) { + $currentPath = implode('/', $parts); + if (isset($this->baseNames[$currentPath])) { + return $currentPath; + } + array_pop($parts); + } + + return null; + } + + /** + * @param string $baseName + */ + private function addBaseName($baseName) + { + if (isset($this->baseNames[$baseName])) { + return; + } + $this->baseNames[$baseName] = realpath($baseName); + } + + /** + * Finds confirmed(!) invocations by alias. + * + * @param string $path + * @return null|PharInvocation + * @see \TYPO3\PharStreamWrapper\PharStreamWrapper::collectInvocation() + */ + private function findByAlias($path) + { + $possibleAlias = $this->resolvePossibleAlias($path); + if ($possibleAlias === null) { + return null; + } + return Manager::instance()->getCollection()->findByCallback( + function (PharInvocation $candidate) use ($possibleAlias) { + return $candidate->isConfirmed() && $candidate->getAlias() === $possibleAlias; + }, + true + ); + } +} diff --git a/modules/system/system.module b/modules/system/system.module index 6a21c1de22e..68b1c86a145 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '6.49'); +define('VERSION', '6.52'); /** * Core API compatibility. diff --git a/modules/user/user.module b/modules/user/user.module index 571db18083f..8fc5c8af1df 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -818,7 +818,7 @@ function user_block($op = 'list', $delta = 0, $edit = array()) { // Display a list of currently online users. $max_users = variable_get('user_block_max_list_count', 10); if ($authenticated_count && $max_users) { - $authenticated_users = db_query_range('SELECT u.uid, u.name, MAX(s.timestamp) AS timestamp FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.timestamp >= %d AND s.uid > 0 GROUP BY u.uid, u.name ORDER BY s.timestamp DESC', $interval, 0, $max_users); + $authenticated_users = db_query_range('SELECT u.uid, u.name, MAX(s.timestamp) AS timestamp FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.timestamp >= %d AND s.uid > 0 GROUP BY u.uid, u.name ORDER BY timestamp DESC', $interval, 0, $max_users); while ($account = db_fetch_object($authenticated_users)) { $items[] = $account; } From 424a2818710ff526f5057e22c59237cc9b1f61c1 Mon Sep 17 00:00:00 2001 From: David Snopek Date: Wed, 20 May 2020 09:32:16 -0500 Subject: [PATCH 5/5] Apply the changes from D6LTS 6.53 --- includes/common.inc | 1 + includes/session.inc | 3 + misc/jquery-html-prefilter-3.5.0-backport.js | 251 +++++++++++++++++++ modules/system/system.module | 2 +- update.php | 2 +- 5 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 misc/jquery-html-prefilter-3.5.0-backport.js diff --git a/includes/common.inc b/includes/common.inc index 78d048ec38d..f70e0e1ae18 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -2244,6 +2244,7 @@ function drupal_add_js($data = NULL, $type = 'module', $scope = 'header', $defer '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(), 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/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