每一個專業的 PHP 開發者都知道用戶上傳的文件都是極其危險的。不管是後端和前端的黑客均可以利用它們搞事情。php
大約在一個月前,我在 reddit
上看了一篇 PHP 上傳漏洞檢測 ,所以, 我決定寫一篇文章。用戶 darpernter 問了一個棘手的問題:前端
儘管我將其重命名爲 'helloworld.txt', 攻擊者是否仍然可以運行他的php 腳本?
置頂的答覆是:laravel
若是文件後綴修改成 .txt ,那麼它不會被當作php文件執行,這樣你安心了吧,不過再三確保不是 .php.txt 的後綴上傳。
很差意思,問題的正確答案並不是如此 . 雖然上面的答覆並不是所有錯誤,但顯然不全面。讓人驚訝的是,大多數的答案都很是類似。數據庫
我想解釋清楚這個問題。因此我要討論的東西變得有點大,我決定讓它變得更大。後端
人們容許用戶上傳文件,可是擔憂用戶上傳的文件在服務器上被執行。瀏覽器
從 php 文件如何被執行開始看。假設一個有 php 環境的服務器,那麼它一般有兩種方法在外部執行 php 文件。一是直接用 URL 請求文件,像 http://example.com/somefile.php
。第二種是 php 如今經常使用的,將全部請求轉發到 index.php
,並在這個文件中以某種方式引入其餘文件。因此,從 php 文件中運行代碼有兩種方式:執行文件或用 include/include_once/require/require_once 的方法引入其餘須要運行的文件。緩存
其實還有第三種方法:eval() 函數。它能將傳入的字符串當作 php 代碼執行。這個函數在大多數 CMS 系統中被用來執行存儲在數據庫裏的代碼。eval() 函數很是危險,但若是你用了它,一般就意味着你確認本身在作危險的操做,並確認你已經沒有其餘選擇。實際上, eval() 有它的用途,而且在某些狀況下很是有用。但若是你是新手的話,我不推薦你使用它。請看 這篇在 OWASP 的文章。我在上面寫了不少。安全
因此,有兩種方法執行文件裏的代碼:直接執行或者在被執行的文件中引入它。那麼如何避免這種事情發生呢?服務器
咱們怎樣才能知道一個文件包含 php 代碼呢?看拓展名,若是以 .php
結尾的,像 somefile.php
咱們就認爲它裏面有 php 代碼。函數
若是在網站根目錄下有一個 somefile.php
文件,那麼在瀏覽器訪問 http://example.com/somefile.php
,這個文件就會被執行而且輸出內容到瀏覽器上。
可是若是我重命名這個文件會怎樣?若是我把它重命名爲 somefile.txt
或者是 somefile.jpg
呢?我會獲得什麼?我會獲得它的內容。它不會被執行。它會從硬盤(或者緩存)直接被髮送過來。
在這點上 reddit 社區上的答案是對的。重命名能防止一個文件被非預期的執行,那麼爲何我認爲這種解決方法是錯的呢?
我相信你注意到我在 「解決方法」 後面加的問號。這個問號是有意義的。如今大多數網站的 URL 上幾乎看不到單獨的 php 文件。而且就算有,也是人爲故意僞造的,由於 URL 上須要有 .php
來實現對老版本 URL 的向後兼容。
如今絕大部分 php 代碼是在運行中被引入的,由於全部請求都被髮送到了網站根目錄的 index.php
。這個文件會根據特定的規則引入其餘 php 文件。這種規則可能(或者在未來會)被惡意使用。若是你應用的規則容許引入用戶的文件,那麼應用會容易遭到攻擊,你應該當即採起措施防止用戶的文件被執行。
重命名文件名能夠嗎? --- 不,辦不到!
PHP解析器不關心文件的後綴名。事實上,全部程序都不關心。雙擊文件,文件會被對應的程序打開。文件後綴名只是幫助操做系統識別用什麼程序打開文件。只要程序有讀取文件的能力,程序就能夠打開任何文件。有時程序拒絕打開和操做文件。但那並非由於後綴名,是文件內容所致。
服務器一般被設置成執行 .php
文件並將執行結果回覆輸出。若是你請求圖片 .jpg
--- 將從磁盤上原樣的返回。若是你要求服務器以某種方式運行一張 jpeg 圖片,會發生?服務器會執行仍是不呢?
圖片來源: Echo / Cultura / Getty Images
程序不關心文件名。甚至不關心文件是否有名字,也不關心它到底是不是文件。
有至少兩個狀況可讓PHP執行代碼:
<?php
和 ?>
標記之間<?=
和 ?>
標記之間即便文件中填充了一些奇怪的二進制數據或一些奇怪的保護名稱,該標記中的代碼仍然會被執行。
這裏有一個圖片給您:
該圖片沒有問題
它如今很純淨。可是您可能知道 JPEG 格式容許在文件中添加一些註釋。好比,拍攝照片的相機型號或座標地址。若是咱們試圖在裏面放一些PHP代碼並嘗試 include 或 require 呢?讓咱們來看看吧!
下載這個圖片到你的硬盤上。或者你本身去弄一張 JPEG 圖片也行。你隨便用什麼格式的文件都無所謂。我建議用一個 JPEG 文件來演示,主要是由於它是一張圖片且易於在其中進行文本編輯。我用的是一個 Windows的筆記本,目前我手頭上沒有 Apple 或 Linux(或其餘UNIX系的系統)的筆記本。因此一會我會發一個這個 OS 下的屏幕快照。可是我確信你確定也能作這個事。
用如下這段 PHP 代碼建個文件:
<h1>Problem?</h1> <img src="troll-face.jpg"> <?php include "./troll-face.jpg";
troll-face.jpg
若是你把你的 php 文件命名爲 index.php
,而後把它放在文件根目錄或者放在你網站目錄下的任何一個文件目錄中。
若是你準確完成了上述步驟,你就能夠看到這個畫面:
到此這都沒毛病。沒 PHP 代碼展現,也沒有 PHP 代碼被執行。
如今,咱們來添加一個問題:
<?php echo "<h2>Yep, a problem!</h2>"; phpinfo(); ?>
刷新頁面!
很明顯出現了一點問題!
您在頁面上看到了該圖片。相同的圖片還存在頁面的 PHP 代碼中。圖片的代碼也被執行了。
長話短說: 若是咱們不在程序種引入這些不安全的文件,文件中的腳本就不會執行。
仔細看下面的例子。
若是有人在某處看到我錯了 - 請糾正我,這是一個嚴重的問題。
PHP是一種腳本語言。您老是須要引用一些動態組合路徑的文件。所以,爲了保護服務器,您必須檢查路徑並防止混淆您的站點文件和用戶上傳或建立的文件。若是用戶的文件與應用程序文件分開,則能夠在使用上傳或建立文件以前檢查文件的路徑。若是它位於您的應用程序腳本容許的文件夾中 - 那麼它可使用 include_once 或 require 或 require_once 引入這個文件。若是不是--那麼就不引入它。
如何進行檢查?這很簡單。你只須要將 $folder
(文件)路徑與一個容許程序引入文件 ( $file
) 的路徑文件夾進行比較。
// 很差的例子,不要用! if (substr($file, 0, strlen($folder)) === $folder) { include $file; }
若是 $folder
的存放路徑是 /path/to/folder
並且 $file
的存放路徑是 /path/to/folder/and/file
, 而後咱們在代碼中使用 substr() 函數把他們的路徑都變成字負串進行判斷,若是文件位於不一樣的文件夾中---這個字符串將不相等。反之則反。
上面的代碼有兩個重要的問題。若是 file
路徑是 /path/to/folderABC/and/file
,很明顯,該文件也不在容許引入的文件夾中。經過向兩個路徑添加斜槓能夠防止這種狀況。咱們在這裏向文件路徑添加斜槓並不重要,由於咱們只須要比較兩個字符串。
舉個例子: 若是 folder
路徑是 /path/to/folder
而且 file
路徑是 /path/to/folder/and/file
,那麼從 file
提取和 folder
具備相同數量的字符,那麼 $ folder
將是 /path/to/folder
。
再好比 folder
路徑是 /path/to/folder
而且 file
路徑是 /path/to/folderABC/and/file
, 那麼從 file
中提取 folder
具備相同數量的字符,和 $folder
同樣,而且將再次成爲/path/to/folder
,這種都是錯誤的,這不是咱們指望的結果。
所以,在 /path/to/folder/
添加斜槓後,與 /path/to/folder/and/file
的提取部分 /path/to/folder/
相同就是安全的。
若是將 /path/to/folder/
與 /path/to/folderABC/and/file
的提取部分 / path/to/folderA
,很明顯二個字符串不同。
這就是咱們指望獲得的。但還有另外一個問題。這並不明顯。我敢確定,若是我問你,你看到這裏有一個災難性的漏洞 - 你不會猜到它在哪裏。你也許已經在經驗中使用過這個東西,甚至可能就在今天。如今,您將看到漏洞是如何隱晦和顯而易見。往下看。
假想一個很常見的場景。
有這麼一個網站。用戶能夠上傳文件到該站點。全部的文件都位於一個特定的目錄下。有一個包含用戶文件的腳本。腳本自上而下進行查找是否包含用戶的輸入(直接或間接)路徑---那這個腳本能夠經過以下方式進行路徑僞造:
/path/to/folder/../../../../../../../another/path/from/root/
舉例。用戶發起請求,你的腳本中包含了一個基於相似以下用戶輸入路徑的文件:
include $folder . "/" . $_GET['some']; // or $_POST, or whatever
你麻煩大了。有天用戶發送一個 ../../../../../../etc/.passwd
這種或其餘請求,你就哭吧。
再否則。假若有人讓你的腳本加載一個他想要的文件,你就廢了。它不必定就只是出如今用戶文件中。它多是你的CMS或你本身文件的一些插件(別相信任何人),甚至是應用程序邏輯中的錯誤等。
用戶可能會上傳一個名爲 file.php
的文件,你會把它和其餘的用戶文件同樣放在一個特定的文件夾裏面:
move_uploaded_file($filename, $folder . '/' . $filename);
用戶的文件就存放在那裏,你必須經常檢查歷來沒有包含該文件夾中的文件,目前來看,全部的東西都挺正常的。一般,用戶發給你的文件不會包含斜槓或者其餘特殊字符,由於這是被系統文件系統禁止的。之因此這樣,是由於一般狀況下瀏覽器發給你的文件是在真實文件系統中建立的,同時它的名字是一些真實存在的文件的名字。
可是 http 請求容許用戶發送任何字符。因此若是某人僞造請求建立名爲 ../../../../../../var/www/yoursite.com/index.php
的文件---這行代碼會覆蓋你的 index.php
文件,若是 index.php
處於在上述路徑的話。
全部的初學者都但願經過過濾 「..」或者斜槓來解決這個問題,可是這種作法是錯誤的,因爲你在安全方面還缺少經驗。同時你必須(是的,必須)明白一個簡單的事情:你永遠沒法在安全和密碼學方面的得到足夠的知識。這句話的意思是,若是你懂得了「兩個點和斜槓」的漏洞,但這不表明你知道全部其餘的缺陷、攻擊和其餘特殊字符,你也不知道在文件寫入文件系統或數據庫時可能發生的代碼轉換。
爲了解決這個問題,PHP中內置了一些特殊函數方法,只是爲了在這種狀況下使用。
第一個解決方案 --- basename() 它從路徑結束時提取路徑的一部分,直到它遇到第一個斜槓,但忽略字符串末尾的斜槓,參見示例。不管如何,你會收到一個安全的文件名。若是你以爲安全 - 那麼是的這很安全。若是它被不法上傳利用 - 你可使用它來校驗文件名是否安全。
另外一個解決方案 --- realpath()它將上傳文件路徑轉換規範化的絕對路徑名,從根開始,而且根本不包含任何不安全因素。它甚至會將符號連接轉換爲此符號連接指向的路徑。
所以,您可使用這兩個函數來檢查上傳文件的路徑。要檢查這個文件路徑究竟是否真正屬於此文件夾路徑。
我編寫了一個函數來提供如上的檢查。我並非專家,因此風險請自行承擔。代碼以下。
<?php /** * Example for the article at medium.com * Created by Igor Data. * User: igordata * Date: 2017-01-23 * @link https://medium.com/@igordata/php-running-jpg-as-php-or-how-to-prevent-execution-of-user-uploaded-files-6ff021897389 Read the article */ /** * 檢查某個路徑是否在指定文件夾內。若爲真,返回此路徑,不然返回 false。 * @param String $path 被檢查的路徑 * @param String $folder 文件夾的路徑,$path 必須在此文件夾內 * @return bool|string 失敗返回 false,成功返回 $path * */ function checkPathIsInFolder($path, $folder) { if ($path === '' OR $path === null OR $path === false OR $folder === '' OR $folder === null OR $folder === false) { /* 不能使用 empty() 由於有可能像 "0" 這樣的字符串也是有效的路徑 */ return false; } $folderRealpath = realpath($folder); $pathRealpath = realpath($path); if ($pathRealpath === false OR $folderRealpath === false) { // Some of paths is empty return false; } $folderRealpath = rtrim($folderRealpath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; $pathRealpath = rtrim($pathRealpath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; if (strlen($pathRealpath) < strlen($folderRealpath)) { // 文件路徑比文件夾路徑短,那麼這個文件不可能在此文件夾內。 return false; } if (substr($pathRealpath, 0, strlen($folderRealpath)) !== $folderRealpath) { // 文件夾的路徑不等於它必須位於的文件夾的路徑。 return false; } // OK return $path; }
結語。
basename($filename)
組成。文件被寫入以前,必定要檢查最終組成的文件路徑。不要信任用戶。不要信任瀏覽器。構建彷佛全部人都在提交病毒的後端。
固然,也沒必要懼怕,這其實比看起來的簡單。只要記住 「不要信任用戶」 以及 「有功能解決此問題」 即可。
轉自 PHP / Laravel 開發者社區 https://laravel-china.org/top...