typo3 / phar-stream-wrapper
PHP原生phar://流处理的拦截器
Requires
- php: ^7.0 || ^8.0
- ext-json: *
Requires (Dev)
- ext-xdebug: *
- phpspec/prophecy: ^1.10
- symfony/phpunit-bridge: ^5.1
Suggests
- ext-fileinfo: For PHP builtin file type guessing, otherwise uses internal processing
README
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
进行了处理。
- https://blog.secarma.co.uk/labs/near-phar-dangerous-unserialization-wherever-you-are
- https://youtu.be/GePBmsNJw6Y
- https://typo3.org/security/advisory/typo3-psa-2018-001/
- https://typo3.org/security/advisory/typo3-psa-2019-007/
- https://typo3.org/security/advisory/typo3-psa-2019-008/
随着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安全团队。