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;}

该问题导致编译后的模板,失效时不被自动清理

 

 

 

本书共94小节:

评论 (写第一个评论)