78. PHP代码储存PhpStorage
在做项目时,有时需要储存php代码,由于她是可执行的,我们并不希望被随意执行或者修改,drupal提供了一个php代码储存组件来保障这一点,她使用文件系统储存,本篇讲解她的使用和原理。
前备知识点:
首先我们需要明确知道文件系统操作的以下几点:
一个文件有三个时间:
创建时间、修改时间、最后访问时间,
她们分别对应php函数:
filectime()、filemtime()、fileatime()
修改时间是本篇的重点
更改文件名不会引起文件修改时间的变化,只有文件内容有变化才会
更改目录下文件的修改时间,不会引起目录修改时间的变化
php程序可以任意修改文件的修改时间
drupal提供的php代码储存组件:
她位于:\core\lib\Drupal\Component\PhpStorage,以组件方式提供,这意味着不依赖其他子系统,可以单独用于drupal以外的项目。
该组件使用文件系统来储存php代码,使用“.php”扩展名,并不以真实文件名来保存或加载内容代码,而是采用虚拟文件名(也可以叫做识别标志符,类似缓存id),真实文件名是经过哈希运算得出的;该组件对保存的代码文件提供两方面保护:
1、保护储存的代码不被浏览器直接访问
2、在通过该组件加载代码时保证代码不被非法修改
权限控制:
第一点是利用服务器的权限配置来做的,在储存代码时,会在其目录下放置“.htaccess”文件,该文件内容如下:
# Deny all requests from Apache 2.4+.
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
# Deny all requests from Apache 2.0-2.2.
<IfModule !mod_authz_core.c>
Deny from all
</IfModule>
# Turn off all options we don't need.
Options -Indexes -ExecCGI -Includes -MultiViews
# Set the catch-all handler to prevent scripts from being executed.
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006
<Files *>
# Override the handler again if we're run later in the evaluation list.
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003
</Files>
# If we know how to do it safely, disable the PHP engine entirely.
<IfModule mod_php5.c>
php_flag engine off
</IfModule>
因为有该文件的存在,在浏览器中直接访问代码会提示权限拒绝,但是这里需要注意,如果服务器被设置为不允许通过“.htaccess”文件进行配置覆写时(如:AllowOverride none),该保障会失效,因此在未知服务器上,我们需要考虑直接运行php代码带来的风险,在储存的php文件中需要进行必要的逻辑保障。
防止php代码被直接执行为什么不采用其他扩展名方式呢?因为在未知服务器上可能引起文件下载
防止非法修改:
被该组件保存的代码,如果经过第三方修改,那么对于组件来说就已经失效了,不会被加载,注意如果不是通过组件加载那么依然是可以的,但为了安全,系统不应当这样做,这里的防止非法修改并不是实时监测文件改动,也不是记录文件哈希,如MD5值等,这样的实现成本比较高,组件采用的原理如下:
组件将php文件储存在一个单独的目录中,目录名采用加载php文件的标识符(虚拟文件名),一个目录只保存一个有效php文件,每当保存文件时将目录的修改时间设置为文件的保存时间,也就是说有效php文件的修改时间和她的目录修改时间是一致的,文件名是经过计算的哈希值,由标识符(目录名)、密钥、目录修改时间经过哈希运算得出,如果第三方修改了文件,那么将引起文件修改时间变化,该时间值会大于目录修改时间,组件借此判断文件已经失效,如果同时更改目录和文件的修改时间,那么会引起文件名和计算后的文件名不一样,因此也会导致文件失效,因为不知道运算文件名的密钥也无法产生新文件。
这里你可能会想到:如果第三方修改了文件,然后将文件修改时间设置回修改前的值呢?这样确实是可以绕过保护的,但能够做到这一点说明恶意攻击者已经可以运行恶意代码,出现了其他安全问题,那么组件的保护就已经没有意义了。
组件的使用:
该组件使用示例如下(可在控制器中测试):
$config = [
'secret' => "passworld", //运算文件名的密钥
'directory' => "phpdir", //储存文件的一级目录,公共文件目录
'bin' => "yunke", //储存文件的二级目录 储存器专用目录
];
$phpCode = <<<EOF
<?php
echo "yunke20180625";
EOF;
$phpfile = "myphp"; //储存标识符,虚拟文件名,也是储存目录名
$storage = new \Drupal\Component\PhpStorage\MTimeProtectedFileStorage($config);
$storage->save($phpfile, $phpCode);
$storage->load($phpfile);
在这个演示中真实文件会保存到如下目录:
phpdir\yunke\myphp
文件名类似如下:
mrIbnISgKvl_DPLojy79ZLeoEDDRL4G4DXj-yagHabQ.php
还具备其他方法,详见接口:
\Drupal\Component\PhpStorage\PhpStorageInterface
这里对具备的方法说明如下($name是文件标识符,以上例为背景):
$storage->exists($name);
判断某个文件是否存在
$storage->load($name);
以include_once方式加载执行php文件,并不是以读取字符串方式
$storage->save($name, $code);
保存代码到文件
$storage->writeable();
返回bin是否可写,默认可写,可以继承并实现自己的逻辑
$storage->delete($name);
删除文件
$storage->deleteAll();
删除bin中的全部文件,包括bin目录
$storage->getFullPath($name);
得到文件全路径,包括文件名
$storage->listAll();
列出bin中的全部内容,返回一个由标识符构成的数组
$storage->garbageCollection();
清理失效文件,当同一个标识符再次保存时,会产生新的文件,以前的文件虽然失效,但不会被自动删除,可以调用该方法清理bin中所有失效文件,但当前实现有bug,见补充说明
组件代码:
该组件继承结构如下:
\Drupal\Component\PhpStorage\PhpStorageInterface
定义组件可使用的方法
\Drupal\Component\PhpStorage\FileStorage
没有修改时间保护的基本储存,只设置了访问权限保护,“.htaccess”文件内容就来自这里:
\Drupal\Component\PhpStorage\FileStorage::htaccessLines()
\Drupal\Component\PhpStorage\MtimeProtectedFastFileStorage
有修改时间保护,允许第三方通过file_put_contents等修改文件,但不能改变文件名
\Drupal\Component\PhpStorage\MtimeProtectedFileStorage
有修改时间保护,不允许第三方修改文件
在drupal中的运用:
是上文的列子中可见该组件需要配置数据,如目录、密钥等,在drupal中提供了工厂类:
\Drupal\Core\PhpStorage\PhpStorageFactory::get($name);
该工厂快速得到一个php代码储存器,只需要提供储存器名即可,其他数据在站点配置文件中查找,配置文件中配置数据如下所示:
$settings['php_storage']['default']=[
'class'=>"...",
'secret'=>"...",
'bin'=>"...", //储存bin,默认为本配置的第二级键名
'directory'=>"..." //默认为PublicStream::basePath() . '/php';通常为/sites/default/files/php
];
其中'php_storage'为配置项,其下一级键名“default”是储存器名,为一个储存器名指定配置数据,只需要新增以上数组,将'default'改为储存器的名字即可,'default'有特殊含义,代表默认配置,系统优先查找储存器名对应的配置,如无再查找'default'配置,若还是没有将采用系统默认值,默认值如下:
储存器类class:
默认为'Drupal\Component\PhpStorage\MTimeProtectedFileStorage',可以自定义实现
密钥secret:
默认为\Drupal\Core\Site\Settings::getHashSalt();
bin:
bin代表储存器在公共文件目录下采用的子目录名,该储存器所有数据均在该目录下,默认采用储存器名,也就是传入工厂方法的名字(同时也是配置第二级键名),该项可以单独指定
php代码储存目录directory:
可以是任意目录,默认采用:
\Drupal\Core\StreamWrapper\PublicStream::basePath() . '/php';
往往是:sites/default/files/php
比如系统储存twig编译后的php模板文件时,代码如下:
\Drupal\Core\PhpStorage\PhpStorageFactory::get ('twig');
补充说明:
1,bug:在以下垃圾清理函数中逻辑有问题:
\Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage::garbageCollection
清理时会将“.htaccess”文件删除,并且由于过期文件保存时被设置为只读(0444权限),导致删除不掉,修复方法如下:
在@chmod($directory, 0777);后面加上:@chmod($fileinfo->getPathName(), 0777);,
在删除循环中加入:
if('.htaccess'==$fileinfo->getFilename ())
{ continue;}
该问题导致编译后的模板,失效时不被自动清理