PHP 安全:過濾、驗證和轉義

咱們在開發應用時,通常有個約定:不要信任任何來自不受本身控制的數據源中的數據。例如如下這些外部源:php

  • $_GET
  • $_POST
  • $_REQUEST
  • $_COOKIE
  • $argv
  • php://stdin
  • php://input
  • file_get_contents()
  • 遠程數據庫
  • 遠程API
  • 來自客戶端的數據

全部這些外部源均可能是攻擊媒介,可能會(有意或無心)把惡意數據注入PHP腳本。編寫接收用戶輸入而後渲染輸出的PHP腳本很容易,但是要安全實現的話,須要下一番功夫。我這裏以陳咬金的三板斧爲引子,給你們介紹三招:過濾輸入、驗證數據,以及轉義輸出。html

一、過濾輸入

過濾輸入是指轉義或刪除不安全的字符。在數據到達應用的存儲層以前,必定要過濾輸入數據,這是第一道防線。假如網站的評論框接受HTML,用戶能夠隨意在評論中加入惡意的<script>標籤,以下所示:laravel

<p>
這篇文章頗有用!
</p>
<script>windows.location.href="http://laravelacademy.org";</script>

若是不過濾這個評論,惡意代碼會存入數據庫,而後在網頁中渲染,當用戶訪問這個頁面時,會重定向到可能不安全的釣魚網站(這種攻擊有一個更專業的稱呼:XSS攻擊)。這個簡單示例很好的說明了爲何咱們要過濾不受本身控制的輸入數據。一般咱們要過濾的輸入數據包括HTML、SQL查詢以及用戶資料等。程序員

HTML

咱們可使用PHP提供的htmlentities函數過濾HTML,該函數會將全部HTML標籤字符(&、<、>等)轉化爲對應的HTML實體,以便在應用存儲層取出後安全渲染。可是有時候咱們是容許用戶輸入某些HTML元素的,尤爲是輸入富文本的時候,好比圖片、連接這些,可是htmlentities不能驗證HTML,檢測不出輸入字符串的字符集,故而沒法實現這樣的功能。正則表達式

<?php
$input = "<p><script>alert('Laravel學院');</script></p>";
echo htmlentities($input, ENT_QUOTES, 'UTF-8');

htmlentities的第一個參數表示要處理的HTML字符串,第二個參數表示要轉義單引號,第三個參數表示輸入字符串的字符集編碼。sql

htmlentities相對的是html_entity_decode方法,該方法會將全部HTML實體轉化爲對應的HTML標籤。數據庫

此外,PHP還提供了一個相似的內置函數htmlspecialchars,該函數也是用於將HTML標籤字符轉化爲HTML實體,只是可以轉化的字符有限(參考官方文檔:http://php.net/manual/zh/function.htmlspecialchars.php),若是要轉化全部字符仍是使用htmlentities方法,值得一提的是和htmlentities同樣,htmlspecialchars也有一個與之相對的方法htmlspecialchars_decodewindows

若是想要直接將輸入字符串中的全部HTML標籤去掉,可使用strip_tags方法。數組

若是須要更增強大的過濾HTML功能,可使用HTML Purifier庫,這是一個很強健且安全的PHP庫,專門用於使用指定規則過濾HTML輸入。在Laravel中咱們可使用相應的擴展包來實現過濾功能:http://laravelacademy.org/post/3914.html安全

SQL查詢

有時候應用必須根據輸入數據構建SQL查詢,這些數據可能來自HTTP請求的查詢字符串,也可能來自HTTP請求的URI片斷,一不當心,就有可能被不懷好意的人利用進行SQL注入攻擊(拼接SQL語句對數據庫進行破壞或者獲取敏感信息)。不少初級的程序員可能會這麼寫代碼:

$sql = sprintf(
    'UPDATE users SET password = "%s" WHERE id = %s',
    $_POST['password'],
    $_GET['id']
);

這麼作風險很大,好比某我的經過以下方式對HTTP發送請求:

POST /user?id=1 HTTP/1.1
Content-Length: 17
Content-Type: application/x-www-form-urlencoded

password=abc」;--

這個HTTP請求會把每一個用戶的密碼都設置爲abc,由於不少SQL數據庫把—視做註釋的開頭,因此會忽略後續文本。

在SQL查詢中必定不能使用未過濾的輸入數據,若是要在SQL查詢中使用輸入數據,必定要使用PDO預處理語句(PDO是PHP內置的數據庫抽象層,爲不一樣的數據庫驅動提供統一接口),PDO預處理語句是PDO提供的一個功能,能夠用於過濾外部數據,而後把過濾後的數據嵌入SQL語句,避免出現上述SQL注入問題,此外預處理語句一次編譯屢次運行,能夠有效減小對系統資源的佔用,獲取更高的執行效率。關於PDO後咱們後續還會在數據庫部分重點討論。

值得注意的是,不少現代PHP框架都使用了MVC架構模式,將數據庫的操做封裝到了Model層,框架底層已經作好了對SQL注入的規避,只要咱們使用模型類提供的方法執行對數據庫的操做,基本上能夠避免SQL注入風險。

咱們以Laravel爲例看看底層是如何規避SQL注入的,改寫上面的update語句,代碼會是這樣:

$id = $_GET['id'];
$password = $_POST['password'];
User::find($id)->update(['password'=>bcrypt($password)]);

因爲模型類底層調用的是是查詢構建器的方法,因此最終會調用Builder(Illuminate\Database\Query\Builder)的update方法:

public function update(array $values)
{
    $bindings = array_values(array_merge($values, $this->getBindings()));

    $sql = $this->grammar->compileUpdate($this, $values);

    return $this->connection->update($sql, $this->cleanBindings($bindings));
}

這段代碼傳入參數是要更新的值,而後經過$bindings得到綁定關係,這裏咱們咱們獲取到的應該是包含passwordupdated_at(默認更新時間戳)的數組,而後再經過Grammar(Illuminate\Database\Query\Grammars\Grammar)類的compileUpdate方法生成預處理SQL語句,這裏對應的sql語句是:

update `users` set `password` = ?, `updated_at` = ? where `id` = ?

而後最終將預處理sql語句和對應綁定關係傳遞給數據庫去執行。關於SQL注入咱們還會在後續數據庫部分繼續討論。

用戶資料信息

若是應用中有用戶帳戶,可能就要處理電子郵件地址、電話號碼、郵政編碼等資料信息。PHP預料到會出現這種狀況,所以提供了filter_varfilter_input函數。這兩個函數的參數能使用不一樣的標誌,過濾不一樣類型的輸入:電子郵件地址、URL編碼字符串、整數、浮點數、HTML字符、URL和特定範圍的ASCII字符。

如下示例展現瞭如何過濾電子郵件地址,刪除除字母、數字和!#$%&'*+-/=?^_{|}~@.[]`以外的全部其餘字符:

<?php
$email = 'yaojinbu@163.com';
$emailSafe = filter_var($email, FILTER_SANITIZE_EMAIL);

更多filter_var的使用請參考PHP官方文檔:http://php.net/manual/zh/function.filter-var.php,相應的移除過濾器請參考:http://php.net/manual/zh/filter.filters.sanitize.php

固然,filter_var函數還能夠用於其它表單提交數據的過濾。

 

二、驗證數據

PHP原生實現

 

驗證輸入數據也很重要,與過濾不一樣,驗證不會從輸入數據中刪除信息,而只是確認用戶輸入是否符合預期。若是輸入的是電子郵件地址,則確保用戶輸入的是電子郵件地址;若是須要的是電話號碼,則確保用戶輸入的是電話號碼,這就是驗證要作的事兒。

驗證是爲了保證在應用的存儲層保存符合特定格式的正確數據,若是遇到無效數據,要停止數據存儲操做,並顯示相應的錯誤信息來提醒用戶輸入正確的數據。驗證還能避免數據庫出現潛在錯誤,例如,若是MySQL指望使用DATETIME類型的值,而提供的倒是DATE字符串,那麼MySQL會報錯或使用默認值,無論哪一種處理方式,應用的完整性都受到無效數據的破壞。

要實現輸入數據驗證,咱們能夠把某個FILTER_VALIDATE_*標識傳遞給filter_var函數,PHP提供了驗證布爾值、電子郵件地址、浮點數、整數、IP、正則表達式和URL的標識(詳見http://php.net/manual/en/filter.filters.validate.php)。下面的示例演示瞭如何驗證電子郵件地址:

<?php
$input = 'yaojinbu@163.com';
$isEmail = filter_var($input, FILTER_VALIDATE_EMAIL);
if ($isEmail !== FALSE) {
    echo 'success';
} else {
    echo 'failed';
}

咱們要特別關注filter_var的返回值,若是驗證成功,返回的是要驗證的值,若是驗證失敗,返回false

藉助PHP組件

雖然filter_var函數提供了不少用於驗證的標識,但一招鮮,不能吃遍天,咱們不能依賴它驗證全部數據,除了filter_var函數,還有如下組件能夠幫助咱們完成更加複雜的驗證功能:

注:輸入數據既要驗證也要過濾,以確保其符合預期且安全

三、PHP 轉義實現

把輸出渲染成網頁或API響應時,必定要轉義輸出,這也是一種防禦措施,能避免渲染惡意代碼,形成XSS攻擊,還能防止應用的用戶無心中執行惡意代碼。

咱們可使用前面提到的htmlentities函數轉移輸出,該函數的第二個參數必定要使用ENT_QUOTES,讓這個函數轉義單引號和雙引號,並且,還要在第三個參數中指定合適的字符編碼(一般是UTF-8),下面的例子演示瞭如何在渲染前轉義HTML輸出:

<?php
$output = '<p><script>alert(「歡迎來到Laravel學院!")</script></p>';
echo htmlentities($output, ENT_QUOTES, ‘UTF-8');

若是不轉義直接輸出,會彈出提示框:

alert

轉義以後輸出變成:

<p><script>alert("歡迎訪問Laravel學院!");</script></p>

現代PHP支持許多模板引擎,這些模板引擎在底層已經爲了作好了轉義處理,好比如今流行的twig/twigsmarty/smarty都會自動轉義輸出。這種默認處理方式很贊,爲PHP Web應用提供了有力的安全保障。

相關文章
相關標籤/搜索