天天看點

雲客Drupal源碼分析之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;}

該問題導緻編譯後的模闆,失效時不被自動清理

我是雲客,【雲遊天下,做客四方】,聯系方式見首頁,歡迎轉載,但須注明出處

繼續閱讀