Commit ff05c001 authored by xjm's avatar xjm

SA-CORE-2019-007 by Blaklis, oliver.hader, alexpott, mlhess, tim.plunkett, dsnopek, xjm

parent 9735baff
......@@ -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 DRUPAL_ROOT . '/misc/typo3/drupal-security/PharExtensionInterceptor.php';
include_once DRUPAL_ROOT . '/misc/brumann/polyfill-unserialize/src/Unserialize.php';
// Set up a stream wrapper to handle insecurities due to PHP's built-in
// phar stream wrapper.
......
/vendor/
/phpunit.xml
/.composer.lock
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
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.
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.
{
"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"
}
}
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Brumann\Polyfill Test Suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src/</directory>
</whitelist>
</filter>
</phpunit>
<?php
namespace Brumann\Polyfill;
final class Unserialize
{
/**
* @see https://secure.php.net/manual/en/function.unserialize.php
*
* @param string $serialized Serialized data
* @param array $options Associative array containing options
*
* @return mixed
*/
public static function unserialize($serialized, array $options = array())
{
if (PHP_VERSION_ID >= 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);
}
}
......@@ -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`.
......
......@@ -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": {
......
<?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!
*/
use TYPO3\PharStreamWrapper\Resolver\PharInvocation;
interface Collectable
{
/**
* @param PharInvocation $invocation
* @return bool
*/
public function has(PharInvocation $invocation);
/**
* @param PharInvocation $invocation
* @param null $flags
* @return bool
*/
public function collect(PharInvocation $invocation, $flags = null);
/**
* @param callable $callback
* @param bool $reverse
* @return null|PharInvocation
*/
public function findByCallback($callback, $reverse = false);
}
......@@ -11,6 +11,13 @@
* The TYPO3 project - inspiring people to share!
*/
/**
* Helper provides low-level tools on file name resolving. However it does not
* (and should not) maintain any runtime state information. In order to resolve
* Phar archive paths according resolvers have to be used.
*
* @see \TYPO3\PharStreamWrapper\Resolvable::resolve()
*/
class Helper
{
/*
......@@ -54,6 +61,15 @@ public static function determineBaseFile($path)
return null;
}
/**
* @param string $path
* @return bool
*/
public static function hasPharPrefix($path)
{
return stripos($path, 'phar://') === 0;
}
/**
* @param string $path
* @return string
......@@ -61,7 +77,7 @@ public static function determineBaseFile($path)
public static function removePharPrefix($path)
{
$path = trim($path);
if (stripos($path, 'phar://') !== 0) {
if (!static::hasPharPrefix($path)) {
return $path;
}
return substr($path, 7);
......@@ -77,7 +93,7 @@ public static function removePharPrefix($path)
public static function normalizePath($path)
{
return rtrim(
static::getCanonicalPath(
static::normalizeWindowsPath(
static::removePharPrefix($path)
),
'/'
......
<?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\Exception;
class ConjunctionInterceptor implements Assertable
{
/**
* @var Assertable[]
*/
private $assertions;
public function __construct(array $assertions)
{
$this->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;
}
}
......@@ -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';
}
}
<?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\Exception;
use TYPO3\PharStreamWrapper\Manager;
use TYPO3\PharStreamWrapper\Phar\DeserializationException;
use TYPO3\PharStreamWrapper\Phar\Reader;
/**
* @internal Experimental implementation of checking against serialized objects in Phar meta-data
* @internal This functionality has not been 100% pentested...
*/
class PharMetaDataInterceptor implements Assertable
{
/**
* Determines whether the according Phar archive contains
* (potential insecure) serialized objects.
*
* @param string $path
* @param string $command
* @return bool
* @throws Exception
*/
public function assert($path, $command)
{
if ($this->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;
}
}
......@@ -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;
}
}