Skip to content
Snippets Groups Projects
Commit bfde12fd authored by Chris Gross's avatar Chris Gross
Browse files

7.x-1.10 Release

parent e89ec060
No related branches found
Tags 7.x-1.10
No related merge requests found
Showing
with 1428 additions and 8563 deletions
Drupal 7.62, 2019-01-15
-----------------------
- Fixed security issues:
- SA-CORE-2019-001
- SA-CORE-2019-002
Drupal 7.61, 2018-11-07
-----------------------
- File upload validation functions and hook_file_validate() implementations are
......
......@@ -8,7 +8,7 @@
/**
* The current system version.
*/
define('VERSION', '7.61');
define('VERSION', '7.62');
/**
* Core API compatibility.
......@@ -704,6 +704,19 @@ function drupal_environment_initialize() {
// Set sane locale settings, to ensure consistent string, dates, times and
// numbers handling.
setlocale(LC_ALL, 'C');
// 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 DRUPAL_ROOT . '/includes/file.phar.inc';
file_register_phar_wrapper();
}
}
}
/**
......
This diff is collapsed.
......@@ -1538,7 +1538,7 @@ function file_save_upload($form_field_name, $validators = array(), $destination
// rename filename.php.foo and filename.php to filename.php.foo.txt and
// filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
// evaluates to TRUE.
if (!variable_get('allow_insecure_uploads', 0) && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $file->filename) && (substr($file->filename, -4) != '.txt')) {
if (!variable_get('allow_insecure_uploads', 0) && preg_match('/\.(php|phar|pl|py|cgi|asp|js)(\.|$)/i', $file->filename) && (substr($file->filename, -4) != '.txt')) {
$file->filemime = 'text/plain';
// The destination filename will also later be used to create the URI.
$file->filename .= '.txt';
......
<?php
use Drupal\Core\Security\PharExtensionInterceptor;
use TYPO3\PharStreamWrapper\Manager as PharStreamWrapperManager;
use TYPO3\PharStreamWrapper\Behavior as PharStreamWrapperBehavior;
use TYPO3\PharStreamWrapper\PharStreamWrapper;
/**
* Registers a phar stream wrapper that is more secure than PHP's built-in one.
*
* @see file_get_stream_wrappers()
*/
function file_register_phar_wrapper() {
$directory = DRUPAL_ROOT . '/misc/typo3/phar-stream-wrapper/src';
include_once $directory . '/Assertable.php';
include_once $directory . '/Behavior.php';
include_once $directory . '/Exception.php';
include_once $directory . '/Helper.php';
include_once $directory . '/Manager.php';
include_once $directory . '/PharStreamWrapper.php';
include_once DRUPAL_ROOT . '/misc/typo3/drupal-security/PharExtensionInterceptor.php';
// Set up a stream wrapper to handle insecurities due to PHP's built-in
// phar stream wrapper.
try {
$behavior = new PharStreamWrapperBehavior();
PharStreamWrapperManager::initialize(
$behavior->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');
}
<?php
namespace Drupal\Core\Security;
use TYPO3\PharStreamWrapper\Assertable;
use TYPO3\PharStreamWrapper\Helper;
use TYPO3\PharStreamWrapper\Exception;
/**
* An alternate PharExtensionInterceptor to support phar-based CLI tools.
*
* @see \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor
*/
class PharExtensionInterceptor implements Assertable {
/**
* Determines whether phar file is allowed to execute.
*
* The phar file is allowed to execute if:
* - the base file name has a ".phar" suffix.
* - it is the CLI tool that has invoked the interceptor.
*
* @param string $path
* The path of the phar file to check.
*
* @param string $command
* The command being carried out.
*
* @return bool
* TRUE if the phar file is allowed to execute.
*
* @throws Exception
* Thrown when the file is not allowed to execute.
*/
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
* 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';
}
}
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.
[![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).
{
"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/"
}
}
}
<?php
namespace TYPO3\PharStreamWrapper;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
interface Assertable
{
/**
* @param string $path
* @param string $command
* @return bool
*/
public function assert($path, $command);
}
<?php
namespace TYPO3\PharStreamWrapper;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
class Behavior implements Assertable
{
const COMMAND_DIR_OPENDIR = 'dir_opendir';
const COMMAND_MKDIR = 'mkdir';
const COMMAND_RENAME = 'rename';
const COMMAND_RMDIR = 'rmdir';
const COMMAND_STEAM_METADATA = 'stream_metadata';
const COMMAND_STREAM_OPEN = 'stream_open';
const COMMAND_UNLINK = 'unlink';
const COMMAND_URL_STAT = 'url_stat';
/**
* @var string[]
*/
private $availableCommands = array(
self::COMMAND_DIR_OPENDIR,
self::COMMAND_MKDIR,
self::COMMAND_RENAME,
self::COMMAND_RMDIR,
self::COMMAND_STEAM_METADATA,
self::COMMAND_STREAM_OPEN,
self::COMMAND_UNLINK,
self::COMMAND_URL_STAT,
);
/**
* @var Assertable[]
*/
private $assertions;
/**
* @param Assertable $assertable
* @return static
*/
public function withAssertion(Assertable $assertable)
{
$commands = func_get_args();
array_shift($commands);
$this->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
);
}
}
<?php
namespace TYPO3\PharStreamWrapper;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
class Exception extends \RuntimeException
{
}
<?php
namespace TYPO3\PharStreamWrapper;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
class Helper
{
/*
* Resets PHP's OPcache if enabled as work-around for issues in `include()`
* or `require()` calls and OPcache delivering wrong results.
*
* @see https://bugs.php.net/bug.php?id=66569
*/
public static function resetOpCache()
{
if (function_exists('opcache_reset')
&& function_exists('opcache_get_status')
) {
$status = opcache_get_status();
if (!empty($status['opcache_enabled'])) {
opcache_reset();
}
}
}
/**
* Determines base file that can be accessed using the regular file system.
* For e.g. "phar:///home/user/bundle.phar/content.txt" that would result
* into "/home/user/bundle.phar".
*
* @param string $path
* @return string|null
*/
public static function determineBaseFile($path)
{
$parts = explode('/', static::normalizePath($path));
while (count($parts)) {
$currentPath = implode('/', $parts);
if (@is_file($currentPath)) {
return $currentPath;
}
array_pop($parts);
}
return null;
}
/**
* @param string $path
* @return string
*/
public static function removePharPrefix($path)
{
$path = trim($path);
if (stripos($path, 'phar://') !== 0) {
return $path;
}
return substr($path, 7);
}
/**
* Normalizes a path, removes phar:// prefix, fixes Windows directory
* separators. Result is without trailing slash.
*
* @param string $path
* @return string
*/
public static function normalizePath($path)
{
return rtrim(
static::getCanonicalPath(
static::removePharPrefix($path)
),
'/'
);
}
/**
* Fixes a path for windows-backslashes and reduces double-slashes to single slashes
*
* @param string $path File path to process
* @return string
*/
private static function normalizeWindowsPath($path)
{
return str_replace('\\', '/', $path);
}
/**
* Resolves all dots, slashes and removes spaces after or before a path...
*
* @param string $path Input string
* @return string Canonical path, always without trailing slash
*/
private static function getCanonicalPath($path)
{
$path = static::normalizeWindowsPath($path);
$absolutePathPrefix = '';
if (static::isAbsolutePath($path)) {
if (static::isWindows() && strpos($path, ':/') === 1) {
$absolutePathPrefix = substr($path, 0, 3);
$path = substr($path, 3);
} else {
$path = ltrim($path, '/');
$absolutePathPrefix = '/';
}
}
$pathParts = explode('/', $path);
$pathPartsLength = count($pathParts);
for ($partCount = 0; $partCount < $pathPartsLength; $partCount++) {
// double-slashes in path: remove element
if ($pathParts[$partCount] === '') {
array_splice($pathParts, $partCount, 1);
$partCount--;
$pathPartsLength--;
}
// "." in path: remove element
if ((isset($pathParts[$partCount]) ? $pathParts[$partCount] : '') === '.') {
array_splice($pathParts, $partCount, 1);
$partCount--;
$pathPartsLength--;
}
// ".." in path:
if ((isset($pathParts[$partCount]) ? $pathParts[$partCount] : '') === '..') {
if ($partCount === 0) {
array_splice($pathParts, $partCount, 1);
$partCount--;
$pathPartsLength--;
} elseif ($partCount >= 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
<?php
namespace TYPO3\PharStreamWrapper\Interceptor;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\PharStreamWrapper\Assertable;
use TYPO3\PharStreamWrapper\Helper;
use TYPO3\PharStreamWrapper\Exception;
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';
}
}
<?php
namespace TYPO3\PharStreamWrapper;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
class Manager implements Assertable
{
/**
* @var self
*/
private static $instance;
/**
* @var Behavior
*/
private $behavior;
/**
* @param Behavior $behaviour
* @return self
*/
public static function initialize(Behavior $behaviour)
{
if (self::$instance === null) {
self::$instance = new self($behaviour);
return self::$instance;
}
throw new \LogicException(
'Manager can only be initialized once',
1535189871
);
}
/**
* @return self
*/
public static function instance()
{
if (self::$instance !== null) {
return self::$instance;
}
throw new \LogicException(
'Manager needs to be initialized first',
1535189872
);
}
/**
* @return bool
*/
public static function destroy()
{
if (self::$instance === null) {
return false;
}
self::$instance = null;
return true;
}
/**
* @param Behavior $behaviour
*/
private function __construct(Behavior $behaviour)
{
$this->behavior = $behaviour;
}
/**
* @param string $path
* @param string $command
* @return bool
*/
public function assert($path, $command)
{
return $this->behavior->assert($path, $command);
}
}
<?php
namespace TYPO3\PharStreamWrapper;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
class PharStreamWrapper
{
/**
* Internal stream constants that are not exposed to PHP, but used...
* @see https://github.com/php/php-src/blob/e17fc0d73c611ad0207cac8a4a01ded38251a7dc/main/php_streams.h
*/
const STREAM_OPEN_FOR_INCLUDE = 128;
/**
* @var resource
*/
public $context;
/**
* @var resource
*/
protected $internalResource;
/**
* @return bool
*/
public function dir_closedir()
{
if (!is_resource($this->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));
}
}
File added
......@@ -2766,4 +2766,64 @@ class StreamWrapperTest extends DrupalWebTestCase {
$this->assertTrue(file_stream_wrapper_valid_scheme(file_uri_scheme('public://asdf')), 'Got a valid stream scheme from public://asdf');
$this->assertFalse(file_stream_wrapper_valid_scheme(file_uri_scheme('foo://asdf')), 'Did not get a valid stream scheme from foo://asdf');
}
/**
* Tests that phar stream wrapper is registered as expected.
*
* @see file_get_stream_wrappers()
*/
public function testPharStreamWrapperRegistration() {
if (!class_exists('Phar', FALSE)) {
$this->assertFalse(in_array('phar', stream_get_wrappers(), TRUE), 'PHP is compiled without phar support. Therefore, no phar stream wrapper is registered.');
}
elseif (version_compare(PHP_VERSION, '5.3.3', '<')) {
$this->assertFalse(in_array('phar', stream_get_wrappers(), TRUE), 'The PHP version is <5.3.3. The built-in phar stream wrapper has been unregistered and not replaced.');
}
else {
$this->assertTrue(in_array('phar', stream_get_wrappers(), TRUE), 'A phar stream wrapper is registered.');
$this->assertFalse(file_stream_wrapper_valid_scheme('phar'), 'The phar scheme is not a valid scheme for Drupal File API usage.');
}
// Ensure that calling file_get_stream_wrappers() multiple times, both
// without and with a drupal_static_reset() in between, does not create
// errors due to the PharStreamWrapperManager singleton.
file_get_stream_wrappers();
file_get_stream_wrappers();
drupal_static_reset('file_get_stream_wrappers');
file_get_stream_wrappers();
}
/**
* Tests that only valid phar files can be used.
*/
public function testPharFile() {
if (!in_array('phar', stream_get_wrappers(), TRUE)) {
$this->pass('There is no phar stream wrapper registered.');
// Nothing else in this test is relevant when there's no phar stream
// wrapper. testPharStreamWrapperRegistration() is sufficient for testing
// the conditions of when the stream wrapper should or should not be
// registered.
return;
}
$base = dirname(dirname(__FILE__)) . '/files';
// Ensure that file operations via the phar:// stream wrapper work for phar
// files with the .phar extension.
$this->assertFalse(file_exists("phar://$base/phar-1.phar/no-such-file.php"));
$this->assertTrue(file_exists("phar://$base/phar-1.phar/index.php"));
$file_contents = file_get_contents("phar://$base/phar-1.phar/index.php");
$expected_hash = 'c7e7904ea573c5ebea3ef00bb08c1f86af1a45961fbfbeb1892ff4a98fd73ad5';
$this->assertIdentical($expected_hash, hash('sha256', $file_contents));
// Ensure that file operations via the phar:// stream wrapper throw an
// exception for files without the .phar extension.
try {
file_exists("phar://$base/image-2.jpg/index.php");
$this->fail('Expected exception failed to be thrown when accessing an invalid phar file.');
}
catch (Exception $e) {
$this->assertEqual(get_class($e), 'TYPO3\PharStreamWrapper\Exception', 'Expected exception thrown when accessing an invalid phar file.');
}
}
}
......@@ -41,7 +41,7 @@
/**
* Note on Drupal 8 porting.
* This file origin is Tar.php, release 1.4.3 (stable) with some code
* This file origin is Tar.php, release 1.4.5 (stable) with some code
* from PEAR.php, release 1.10.5 (stable) both at http://pear.php.net.
* To simplify future porting from pear of this file, you should not
* do cosmetic or other non significant changes to this file.
......@@ -1406,10 +1406,22 @@ class Archive_Tar
if ($p_stored_filename == '') {
$p_stored_filename = $p_filename;
}
$v_reduce_filename = $this->_pathReduction($p_stored_filename);
if (strlen($v_reduce_filename) > 99) {
if (!$this->_writeLongHeader($v_reduce_filename)) {
$v_reduced_filename = $this->_pathReduction($p_stored_filename);
if (strlen($v_reduced_filename) > 99) {
if (!$this->_writeLongHeader($v_reduced_filename, false)) {
return false;
}
}
$v_linkname = '';
if (@is_link($p_filename)) {
$v_linkname = readlink($p_filename);
}
if (strlen($v_linkname) > 99) {
if (!$this->_writeLongHeader($v_linkname, true)) {
return false;
}
}
......@@ -1418,14 +1430,10 @@ class Archive_Tar
$v_uid = sprintf("%07s", DecOct($v_info[4]));
$v_gid = sprintf("%07s", DecOct($v_info[5]));
$v_perms = sprintf("%07s", DecOct($v_info['mode'] & 000777));
$v_mtime = sprintf("%011s", DecOct($v_info['mtime']));
$v_linkname = '';
if (@is_link($p_filename)) {
$v_typeflag = '2';
$v_linkname = readlink($p_filename);
$v_size = sprintf("%011s", DecOct(0));
} elseif (@is_dir($p_filename)) {
$v_typeflag = "5";
......@@ -1437,7 +1445,6 @@ class Archive_Tar
}
$v_magic = 'ustar ';
$v_version = ' ';
if (function_exists('posix_getpwuid')) {
......@@ -1452,14 +1459,12 @@ class Archive_Tar
}
$v_devmajor = '';
$v_devminor = '';
$v_prefix = '';
$v_binary_data_first = pack(
"a100a8a8a8a12a12",
$v_reduce_filename,
$v_reduced_filename,
$v_perms,
$v_uid,
$v_gid,
......@@ -1499,7 +1504,7 @@ class Archive_Tar
$this->_writeBlock($v_binary_data_first, 148);
// ----- Write the calculated checksum
$v_checksum = sprintf("%06s ", DecOct($v_checksum));
$v_checksum = sprintf("%06s\0 ", DecOct($v_checksum));
$v_binary_data = pack("a8", $v_checksum);
$this->_writeBlock($v_binary_data, 8);
......@@ -1531,7 +1536,7 @@ class Archive_Tar
$p_filename = $this->_pathReduction($p_filename);
if (strlen($p_filename) > 99) {
if (!$this->_writeLongHeader($p_filename)) {
if (!$this->_writeLongHeader($p_filename, false)) {
return false;
}
}
......@@ -1627,36 +1632,31 @@ class Archive_Tar
* @param string $p_filename
* @return bool
*/
public function _writeLongHeader($p_filename)
public function _writeLongHeader($p_filename, $is_link = false)
{
$v_size = sprintf("%11s ", DecOct(strlen($p_filename)));
$v_typeflag = 'L';
$v_uid = sprintf("%07s", 0);
$v_gid = sprintf("%07s", 0);
$v_perms = sprintf("%07s", 0);
$v_size = sprintf("%'011s", DecOct(strlen($p_filename)));
$v_mtime = sprintf("%011s", 0);
$v_typeflag = ($is_link ? 'K' : 'L');
$v_linkname = '';
$v_magic = '';
$v_version = '';
$v_magic = 'ustar ';
$v_version = ' ';
$v_uname = '';
$v_gname = '';
$v_devmajor = '';
$v_devminor = '';
$v_prefix = '';
$v_binary_data_first = pack(
"a100a8a8a8a12a12",
'././@LongLink',
0,
0,
0,
$v_perms,
$v_uid,
$v_gid,
$v_size,
0
$v_mtime
);
$v_binary_data_last = pack(
"a1a100a6a2a32a32a8a8a155a12",
......@@ -1691,7 +1691,7 @@ class Archive_Tar
$this->_writeBlock($v_binary_data_first, 148);
// ----- Write the calculated checksum
$v_checksum = sprintf("%06s ", DecOct($v_checksum));
$v_checksum = sprintf("%06s\0 ", DecOct($v_checksum));
$v_binary_data = pack("a8", $v_checksum);
$this->_writeBlock($v_binary_data, 8);
......@@ -1836,10 +1836,13 @@ class Archive_Tar
*/
private function _maliciousFilename($file)
{
if (strpos($file, '/../') !== false) {
if (strpos($file, 'phar://') === 0) {
return true;
}
if (strpos($file, '../') === 0) {
if (strpos($file, DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR) !== false) {
return true;
}
if (strpos($file, '..' . DIRECTORY_SEPARATOR) === 0) {
return true;
}
return false;
......@@ -1904,11 +1907,20 @@ class Archive_Tar
continue;
}
// ----- Look for long filename
if ($v_header['typeflag'] == 'L') {
if (!$this->_readLongHeader($v_header)) {
return null;
}
switch ($v_header['typeflag']) {
case 'L': {
if (!$this->_readLongHeader($v_header)) {
return null;
}
} break;
case 'K': {
$v_link_header = $v_header;
if (!$this->_readLongHeader($v_link_header)) {
return null;
}
$v_header['link'] = $v_link_header['filename'];
} break;
}
if ($v_header['filename'] == $p_filename) {
......@@ -2009,11 +2021,20 @@ class Archive_Tar
continue;
}
// ----- Look for long filename
if ($v_header['typeflag'] == 'L') {
if (!$this->_readLongHeader($v_header)) {
return false;
}
switch ($v_header['typeflag']) {
case 'L': {
if (!$this->_readLongHeader($v_header)) {
return null;
}
} break;
case 'K': {
$v_link_header = $v_header;
if (!$this->_readLongHeader($v_link_header)) {
return null;
}
$v_header['link'] = $v_link_header['filename'];
} break;
}
// ignore extended / pax headers
......
WCM Base 7.x-1.10, 2019-01-16
-----------------------------
- WCM Base: Updated Drupal core to 7.62 per SA-CORE-2019-001.
- WCM News Client: Added news_client_social_image_json field/token handling.
WCM Base 7.x-1.10-rc3, 2019-01-14
---------------------------------
- WCM Base: Updated Panopoly to 1.58.
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment