typo3/phar-stream-wrapper

PHP原生phar://流处理的拦截器

v3.1.7 2021-09-20 19:19 UTC

README

Scrutinizer Code Quality GitHub Build Status Ubuntu GitHub Build Status Windows Downloads

PHP Phar Stream Wrapper

摘要 & 历史

基于Sam Thomas关于与混淆策略结合的不安全反序列化的发现,该策略允许将Phar文件隐藏在有效的图像资源中,TYPO3项目当时决定引入一个PharStreamWrapper来拦截PHP中phar://流的调用,并仅允许在文件系统的定义位置使用。

鉴于TYPO3的使命是激发人们分享,我们认为将我们的PharStreamWrapper作为独立包发布到PHP社区对其他人会有所帮助。

提到的安全问题于2018年6月10日由Sam Thomas报告给TYPO3,并在2018年7月12日的TYPO3版本7.6.30 LTS、8.7.17 LTS和9.3.1中针对特定的攻击向量以及此通用的PharStreamWrapper进行了处理。

随着PHP 8.0.0的发布,默认行为发生了变化,元数据不再自动反序列化

许可证

通常,TYPO3核心是在GNU通用公共许可证版本2或任何更新的版本(GPL-2.0-or-later)下发布的。为了避免许可问题和不兼容性,此PharStreamWrapper在MIT许可证下授权。如果您复制或修改源代码,不需要提供信用,但非常欢迎。

信用

感谢Alex Pott,Drupal创建所有源代码的回滚版本,以提供与PHP v5.3的兼容性。

安装

PharStreamWrapper作为composer包typo3/phar-stream-wrapper提供,其最低要求为PHP v5.3(v2分支)和PHP v7.0(master分支)。

PHP v7.0的安装

composer require typo3/phar-stream-wrapper ^3.0

PHP v5.3的安装

composer require typo3/phar-stream-wrapper ^2.0

示例

以下示例包含在此包中,显示的PharExtensionInterceptor拒绝没有.phar后缀的所有流包装器调用。拦截器逻辑必须是特定的,并调整到相应的要求。

\TYPO3\PharStreamWrapper\Manager::initialize(
    (new \TYPO3\PharStreamWrapper\Behavior())
        ->withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor())
);

if (in_array('phar', stream_get_wrappers())) {
    stream_wrapper_unregister('phar');
    stream_wrapper_register('phar', \TYPO3\PharStreamWrapper\PharStreamWrapper::class);
}
  • PharStreamWrapper定义为一个类引用,每次处理phar://流时都会实例化。
  • Manager作为一个单例模式,由PharStreamWrapper实例调用以检索个别行为和设置。
  • Behavior持有引用拦截器,用于断言给定$path和给定$command的正确/允许的调用。拦截器实现Assertable接口。拦截器可以单独处理以下命令,或者在不具体定义的情况下处理所有命令
    • COMMAND_DIR_OPENDIR
    • COMMAND_MKDIR
    • COMMAND_RENAME
    • COMMAND_RMDIR
    • COMMAND_STEAM_METADATA
    • COMMAND_STREAM_OPEN
    • COMMAND_UNLINK
    • COMMAND_URL_STAT

拦截器

以下拦截器包含在此包中,并可供使用以阻止任何没有.phar后缀的Phar文件调用。当然,也可以有单独的拦截器。

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(string $path, string $command): bool
    {
        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(string $path): bool
    {
        $baseFile = Helper::determineBaseFile($path);
        if ($baseFile === null) {
            return false;
        }
        $fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION);
        return strtolower($fileExtension) === 'phar';
    }
}

ConjunctionInterceptor

此拦截器结合了多个实现Assertable的拦截器。只有当所有嵌套拦截器都成功时,它才会成功(逻辑AND)。

\TYPO3\PharStreamWrapper\Manager::initialize(
    (new \TYPO3\PharStreamWrapper\Behavior())
        ->withAssertion(new ConjunctionInterceptor([
            new PharExtensionInterceptor(),
            new PharMetaDataInterceptor(),
        ]))
);

PharExtensionInterceptor

这个(基本)拦截器只检查被调用的Phar存档是否有相应的.phar文件扩展名。同时考虑了解析符号链接以及Phar内部别名解析。

\TYPO3\PharStreamWrapper\Manager::initialize(
    (new \TYPO3\PharStreamWrapper\Behavior())
        ->withAssertion(new PharExtensionInterceptor())
);

PharMetaDataInterceptor

此拦截器实际上正在检查序列化的Phar元数据与PHP对象之间的匹配,如果在Phar存档中不仅发现标量值,则会将其视为恶意。为了避免触发初始漏洞,使用了自定义的低级Phar\Reader

\TYPO3\PharStreamWrapper\Manager::initialize(
    (new \TYPO3\PharStreamWrapper\Behavior())
        ->withAssertion(new PharMetaDataInterceptor())
);

Reader

  • Phar\Reader::__construct(string $fileName):为Phar存档创建低级读取器
  • Phar\Reader::resolveContainer(): Phar\Container:解析表示Phar存档的模型
  • Phar\Container::getStub(): Phar\Stub:解析Phar存档的(纯PHP)stub部分
  • Phar\Container::getManifest(): Phar\Manifest:解析如https://php.ac.cn/manual/en/phar.fileformat.manifestfile.php所记录的Phar存档清单
  • Phar\Stub::getMappedAlias(): string:解析stub中定义的Phar存档内部别名,使用Phar::mapPhar('alias.phar') - 实际上在这里分析纯PHP源代码
  • Phar\Manifest::getAlias(): string - 解析清单中定义的Phar存档内部别名,使用Phar::setAlias('alias.phar')
  • Phar\Manifest::getMetaData(): string:解析序列化的Phar存档元数据
  • Phar\Manifest::deserializeMetaData(): mixed:解析只包含标量值的反序列化Phar存档元数据 - 如果确定有对象,将抛出相应的Phar\DeserializationException
$reader = new Phar\Reader('example.phar');
var_dump($reader->resolveContainer()->getManifest()->deserializeMetaData());

Helper

  • Helper::determineBaseFile(string $path): string:确定可以使用常规文件系统访问的基本文件。例如,以下路径phar:///home/user/bundle.phar/content.txt将被解析为/home/user/bundle.phar
  • Helper::resetOpCache():如果启用,则重置PHP的OPcache,作为include()require()调用中问题以及OPcache提供错误结果的解决方案。更多详情可以在PHP的bug tracker中找到,例如https://bugs.php.net/bug.php?id=66569

安全联系人

如果在TYPO3项目或特定于此PharStreamWrapper包中找到额外的安全问题,请联系TYPO3安全团队