1. 程式人生 > >雲客Drupal8原始碼分析之PHP程式碼儲存PhpStorage

雲客Drupal8原始碼分析之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;}
該問題導致編譯後的模板,失效時不被自動清理

我是雲客,【雲遊天下,做客四方】,微訊號:php-world,歡迎轉載,但須註明出處,討論請加qq群203286137