談談 Web 安全

Sandy發表於2019-02-25

攻擊者無時無刻不在準備對你的 Web 應用程式進行攻擊,因此提高你的 Web 應用程式的安全性是非常有必要的。幸運的是,來自開放式 Web 應用程式安全專案 (OWASP) 的有心人已經整理了一份包含了已知安全問題和防禦方式的全面的清單。這份清單對於具有安全意識的開發者來說是必讀的。由 Padraic Brady 著作的 生存手冊:PHP 安全 也是一份很不錯的 PHP 安全閱讀資料。

密碼雜湊

每個人在建構 PHP 應用時終究都會加入使用者登入的模組。使用者的帳號及密碼會被儲存在資料庫中,在登入時用來驗證使用者。

在儲存密碼前正確的 雜湊密碼 是非常重要的。雜湊密碼是單向不可逆的,該雜湊值是一段固定長度的字串且無法逆向推算出原始密碼。這就代表你可以雜湊另一串密碼,來比較兩者是否是同一個密碼,但又無需知道原始的密碼。如果你不將密碼雜湊,那麼當未授權的第三者進入你的資料庫時,所有使用者的帳號資料將會一覽無遺。有些使用者可能(很不幸的)在別的網站也使用相同的密碼。所以務必要重視資料安全的問題。

密碼應該單獨被 加鹽處理 ,加鹽值指的是在雜湊之前先加入隨機子串。以此來防範「字典破解」或者「彩虹碰撞」(一個可以儲存了通用雜湊後的密碼資料庫,可用來逆向推出密碼)。

雜湊和加鹽非常重要,因為很多情況下,使用者會在不同的服務中選擇使用同一個密碼,密碼的安全性很低。

值得慶幸的是,在 PHP 中這些很容易做到。

使用 password_hash 來雜湊密碼

password_hash 函式在 PHP 5.5 時被引入。 此函式現在使用的是目前 PHP 所支援的最強大的加密演算法 BCrypt 。 當然,此函式未來會支援更多的加密演算法。 password_compat 庫的出現是為了提供對 PHP >= 5.3.7 版本的支援。

在下面例子中,我們雜湊一個字串,然後和新的雜湊值對比。因為我們使用的兩個字串是不同的(’secret-password’ 與 ‘bad-password’),所以登入失敗。

<?php
require `password.php`;

$passwordHash = password_hash(`secret-password`, PASSWORD_DEFAULT);

if (password_verify(`bad-password`, $passwordHash)) {
    // Correct Password
} else {
    // Wrong password
}
複製程式碼

password_hash() 已經幫你處理好了加鹽。加進去的隨機子串通過加密演算法自動儲存著,成為雜湊的一部分。password_verify() 會把隨機子串從中提取,所以你不必使用另一個資料庫來記錄這些隨機子串。

資料過濾

永遠不要信任外部輸入。請在使用外部輸入前進行過濾和驗證。filter_var() 和 filter_input() 函式可以過濾文字並對格式進行校驗(例如 email 地址)。

外部輸入可以是任何東西:$_GET 和 $_POST 等表單輸入資料,$_SERVER 超全域性變數中的某些值,還有通過 fopen(`php://input`, `r`) 得到的 HTTP 請求體。記住,外部輸入的定義並不侷限於使用者通過表單提交的資料。上傳和下載的文件,session 值,cookie 資料,還有來自第三方 web 服務的資料,這些都是外部輸入。

雖然外部輸入可以被儲存、組合並在以後繼續使用,但它依舊是外部輸入。每次你處理、輸出、連結或在程式碼中包含時,請提醒自己檢查資料是否已經安全地完成了過濾。

資料可以根據不同的目的進行不同的 過濾 。比如,當原始的外部輸入被傳入到了 HTML 頁面的輸出當中,它可以在你的站點上執行 HTML 和 JavaScript 指令碼!這屬於跨站指令碼攻擊(XSS),是一種很有殺傷力的攻擊方式。一種避免 XSS 攻擊的方法是在輸出到頁面前對所有使用者生成的資料進行清理,使用 strip_tags() 函式來去除 HTML 標籤或者使用 htmlentities() 或是 htmlspecialchars() 函式來對特殊字元分別進行轉義從而得到各自的 HTML 實體。

另一個例子是傳入能夠在命令列中執行的選項。這是非常危險的(同時也是一個不好的做法),但是你可以使用自帶的 escapeshellarg() 函式來過濾執行命令的引數。

最後的一個例子是接受外部輸入來從檔案系統中載入檔案。這可以通過將檔名修改為檔案路徑來進行利用。你需要過濾掉"/""../"null 字元或者其他檔案路徑的字元來確保不會去載入隱藏、私有或者敏感的檔案。

資料清理

資料清理是指刪除(或轉義)外部輸入中的非法和不安全的字元。

例如,你需要在將外部輸入包含在 HTML 中或者插入到原始的 SQL 請求之前對它進行過濾。當你使用 PDO 中的限制引數功能時,它會自動為你完成過濾的工作。

有些時候你可能需要允許一些安全的 HTML 標籤輸入進來並被包含在輸出的 HTML 頁面中,但這實現起來並不容易。儘管有一些像 HTML Purifier 的白名單類庫為了這個原因而出現,實際上更多的人通過使用其他更加嚴格的格式限制方式例如使用 Markdown 或 BBCode 來避免出現問題。

檢視 Sanitization Filters

反序列化 Unserialization

使用 unserialize() 從使用者或者其他不可信的渠道中提取資料是非常危險的事情。這樣做會觸發惡意例項化物件(包含使用者定義的屬性),即使物件沒用被使用,也會觸發執行物件的解構函式。所以你應該避免從不可信渠道反序列化資料。

如果你必須這樣做,請你使用 PHP 7 的 allowed_classes 選項來限制反序列化的物件型別。

有效性驗證

驗證是來確保外部輸入的是你所想要的內容。比如,你也許需要在處理註冊申請時驗證 email 地址、手機號碼或者年齡等資訊的有效性。

檢視 Validation Filters

配置檔案

當你在為你的應用程式建立配置檔案時,最好的選擇時參照以下的做法:

  • 推薦你將你的配置資訊儲存在無法被直接讀取和上傳的位置上。
  • 如果你一定要儲存配置檔案在根目錄下,那麼請使用 .php 的副檔名來進行命名。這將可以確保即使指令碼被直接訪問到,它也不會被以明文的形式輸出出來。
  • 配置檔案中的資訊需要被針對性的保護起來,對其進行加密或者設定訪問許可權。
  • 建議不要把敏感資訊如密碼或者 API 令牌放到版本控制器中。

註冊全域性變數

注意: 自 PHP 5.4.0 開始,register_globals 選項已經被移除並不再使用。這是在提醒你如果你正在升級舊的應用程式的話,你需要注意這一點。

當 register_globals 選項被開啟時,它會使許多型別的變數(包括 $_POST$_GET 和 $_REQUEST)被註冊為全域性變數。這將很容易使你的程式無法有效地判斷資料的來源並導致安全問題。

例如:$_GET[`foo`] 可以通過 $foo 被訪問到,也就是可以對未宣告的變數進行覆蓋。如果你使用低於 5.4.0 版本的 PHP 的話,請 確保 register_globals 是被設為 off 的。

錯誤報告

錯誤日誌對於發現程式中的錯誤是非常有幫助的,但是有些時候它也會將應用程式的結構暴露給外部。為了有效的保護你的應用程式不受到由此而引發的問題。你需要將在你的伺服器上使用開發和生產(線上)兩套不同的配置。

開發環境

為了在 開發 環境中顯示所有可能的錯誤,將你的 php.ini 進行如下配置:

display_errors = On
display_startup_errors = On
error_reporting = -1
log_errors = On
複製程式碼

將值設為 -1 將會顯示出所有的錯誤,甚至包括在未來的 PHP 版本中新增加的型別和引數。 和 PHP 5.4 起開始使用的 E_ALL 是相同的。- php.net

E_STRICT 型別的錯誤是在 5.3.0 中被引入的,並沒有被包含在 E_ALL 中。然而從 5.4.0 開始,它被包含在了 E_ALL中。這意味著什麼?這表示如果你想要在 5.3 中顯示所有的錯誤資訊,你需要使用 -1 或者 E_ALL | E_STRICT

不同 PHP 版本下開啟全部錯誤顯示

  • < 5.3 -1 或 E_ALL
  • 5.3 -1 或 E_ALL | E_STRICT
  • 5.3 -1 或 E_ALL

生產環境

為了在 生產 環境中隱藏錯誤顯示,將你的 php.ini 進行如下配置:

display_errors = Off
display_startup_errors = Off
error_reporting = E_ALL
log_errors = On
複製程式碼

當在生產環境中使用這個配置時,錯誤資訊依舊會被照常儲存在 web 伺服器的錯誤日誌中,唯一不同的是將不再顯示給使用者。更多關於設定的資訊,請參考 PHP 手冊:

相關文章