「構建安全的 PHP 應用」讀書筆記

dailybird發表於2019-02-16

書名:構建安全的 PHP 應用
作者:(美) Ben Edmunds
譯者:張慶龍

以下記錄這本 PHP Web 安全小書的大致內容,對書中的知識點進行備忘。

不要相信任何使用者的任何輸入

SQL 注入攻擊

這是一個老生常談的話題:我們可以利用 SQL 語句本身的作用方式,使用簡單的字串拼接,就能使其執行結果偏離預期,甚至造成毀滅性後果。比如:

`UPDATE User SET name = "`. $name .`" WHERE id = 123`

若此時 $name 的值為 "; DROP DATABASE User; -- ,則拼接之後,實際執行的 SQL 語句為:

UPDATE User SET name = ""; DROP DATABASE User; -- " WHERE id = 123

此時,災難就發生了。

注:-- 表示註釋後續的語句。注意 -- 後有一個空格。這裡使用 # 也可以達到註釋的效果。

解決方法

  1. 在執行前進行敏感字元的過濾(不能只通過 JavaScript);

  2. 為不同的業務模組分配細顆粒度許可權的資料庫連線;

  3. 使用預處理和佔位符,如 $db->prepare() 以及 $db->execute()

  4. 使用儲存過程。但這種做法將部分業務邏輯轉移到了資料庫層,增加了測試和版本控制的難度。

批量賦值陷阱

使用 $_POST 中的所有欄位直接作為資料庫操作的資料,可能會使得攻擊者通過修改表單的提交項,從而實現意外資料的修改,如:

----- xxx.html
<form action="action.php" method="post">
    <input name="username">
    <input name="password">
    
    <!-- 加入新表單項 -->
    <input name="role" value="admin" />
    
    <input type="submit">
</form>

----- action.php
$user = User::create(Input::all());

如上,如果攻擊者在前端頁面中加入了一條新的表單項,在後臺不加區分的情況下,直接把全部資料用於資料庫修改或增加,可能會造成新資料的插入或原資料的修改不如預期。如上例中,本應按照 role 的預設值建立的普通使用者,此時變為了 admin 身份。

解決方法

  1. 欄位對映。資料庫欄位、資料庫檢視欄位、API 介面欄位不完全相同,使得攻擊者難以知曉資料庫欄位的真實名;

  2. 給可以被安全賦值的欄位加上白名單、或給危險欄位加黑名單。如 Laravel 中的 $fillable$guard

型別轉換

PHP 的弱型別在一定程度上提升了開發效率,但也留下了安全隱患。不同型別間(包括資料庫本身)的隱式轉換有可能會使得資料表中的資料與預期不符。

因而我們一定要關注輸入資料的型別,還包括那些在 JavaScript 處理階段被轉換的資料型別。

淨化輸出

轉義標籤

使用 htmlspecialchars()htmlentities() 對如 <, >, & 等特殊字元進行轉移,使得儲存於資料庫中的功能性 HTML 標籤不會直接輸出到瀏覽器(實際上,這一過程在資料輸入的時候也需要進行)。

轉義命令

使用 escapeshellcmd()escapeshellarg() 轉移命令和引數,以確保命令執行的安全可控性。

為什麼要用 HTTPS

HTTPS 指 HTTP Secure 或 HTTP on SSL。HTTPS 可以保證內容的安全性,使得只有最終傳遞到的、具有有效證照的接收者才能得到這一內容。採用 HTTPS 可以有效地預防中間人攻擊和會話劫持。關於 HTTPS 的原理科普可以參考 「也許,這樣理解HTTPS更容易」

HTTPS 的侷限性

普通的虛擬主機配置不能使用在 SSL 上。使用託管主機或在一個伺服器上執行多個站點都會存在問題。這時需要更換為專用伺服器。

此外,HTTPS 在連線階段包含 SSL 握手用於建立連線,因此速度會變慢。但在連線建立完成之後,這個問題就不明顯了。

使用 HTTPS

想要使用 HTTPS,你需要完成以下步驟:

1. 選擇合適的 SSL 證照

子站點較多時,使用通配 SSL 證照。反之使用標準版即可。

2. 生成伺服器證照

首先需要生成私鑰:

openssl genrsa -out yourApp.key 1024

然後使用私鑰生成簽名:

openssl req -new -key yourApp.key -out yourApp.csr

之後需要在證照頒發機構中獲取證照,通常需要使用 yourApp.csr 檔案,這一步獲取的證照為 yourAppSigned.crt

最後就是根據伺服器的型別(Apache、Nginx 或其他)進行對應的配置。在 Apache 中為:

<VirtualHost *.443>
    # ...
    SSLEngine on
    SSLCertificateFile /your/path/to/yourAppSigned.crt
    SSLCertificateKeyFile /your/path/to/yourApp.key
    # ...
</VirtualHost>

在 Nginx 中為:

server {
    listen 443;
    # ...
    ssl  on;
    ssl_certificate /your/path/to/yourAppSigned.crt
    ssl_certificate_key /your/path/to/yourApp.key
    # ...
}

你可以使用以下方式用以正確的適配協議,如:

<link href="//assets/xx.css">

這時,當訪問的 URL 為 http://xxx.com 時,該引用也會是 HTTP 協議;當訪問的 URL 為 https://xxx.com 時,引用會變為 HTTPS 協議。

如何安全的儲存密碼

不要儲存密碼或可逆加密結果,要儲存不可逆的雜湊串。

針對雜湊演算法的攻擊

雖然雜湊方式使得密碼儲存變為密碼值的不可逆串,消除了反向破解的可能。但仍有很多安全隱患。

查詢表

雖然從雜湊後的字串反向解析是不可能的,但通過列舉的方式一個個試探仍然可以得到出正確的密碼。當然,列舉是不現實的,通常的做法是儲存一個查詢表,表項為 密碼 - 雜湊串。然後通過查詢的方式暴力試探和破解。

這一方式可以通過對雜湊過程加「鹽」進行預防,如在密碼進行雜湊前,於密碼中插入一些字元,混合後一起雜湊。

彩虹表

彩虹表在技術上與查詢表類似,但其使用了數學方法用較小的記憶體實現了查詢表。關於彩虹表可以參考 維基百科

碰撞攻擊

碰撞攻擊,即不同的字串的雜湊值相同。在離散數學中,此攻擊又可以稱為「生日攻擊」,以下引用 維基百科

生日問題是指,如果一個房間裡有 23 個或 23 個以上的人,那麼至少有兩個人的生日相同的概率要大於 50% 。這就意味著在一個典型的標準小學班級(30 人)中,存在兩人生日相同的可能性更高。對於 60 或者更多的人,這種概率要大於 99% 。從引起邏輯矛盾的角度來說生日悖論並不是一種悖論,從這個數學事實與一般直覺相牴觸的意義上,它才稱得上是一個悖論。大多數人會認為,23 人中有 2 人生日相同的概率應該遠遠小於 50% 。計算與此相關的概率被稱為生日問題,在這個問題之後的數學理論已被用於設計著名的密碼攻擊方法:生日攻擊。

鹽與隨機

鹽是為了使雜湊唯一而附加在其上的東西。這意味著即使有了雜湊密碼錶,攻擊者也不能正確地匹配上密碼。由此可知,鹽的隨機性是密碼安全的一部分。

雖然 PHP 的內建函式 rand()mt_rand() 可以生成隨機數,但這是使用演算法生成的數字,因而沒有足夠的外部資料使其真正唯一。這意味著採用這兩種函式生成的隨機數可以被攻擊者猜測。事實上,只需要知道 rand() 函式的 624 個值就可以預判之後的所有值了。

使用 /dev/random 在大多數系統中是真正隨機的好方法。它會收集系統熵和環境資料,如鍵盤輸入、硬體資料等。但這一過程會導致阻塞,使得效率極低。在這一情況下,我們可以使用 /dev/urandom,該方法在真正隨機上並不夠強壯,但它作為鹽卻足夠安全。

為了使用隨機而又不儲存鹽的具體值,對應的雜湊方法中,如 crypt() ,返回的結果會包括我們採用的演算法、密碼的雜湊值,以及鹽。

雜湊演算法

MD5

MD5 早已被數學方法證明其並不安全。它很容易在現代硬體上產生衝突。但 MD5 也不是一無是處,配合合適的鹽也可以保證雜湊結果的安全。

SHA-1

同 MD5 一樣,SHA-1 演算法也被證明可以通過不到 2^69 次雜湊產生衝突,因而是不安全的。

SHA-256/SHA-512

二者採用的核心演算法幾乎是一樣的,但 SHA-256 使用 32 位字元,而 SHA-512 採用 64 位,二者的迴圈次數也不相同。

BCrypt

BCrypt 是 Blowfish 密碼的衍生方法。該演算法是迭代的,由於開銷的關係,使其可以防止暴力破解。BCrypt 在加密純文字密碼時有 72 字元的限制,但這一演算法長期以來仍沒有漏洞公佈,因而被認為是密碼安全的。

SCrypt

SCrypt 是一個在記憶體方面加強的衍生演算法。理論上來說,該演算法在高記憶體消耗之下是一個更為安全的演算法。

使用雜湊

在 PHP 5.5 版本之後引入了新的密碼雜湊函式 password_hash()password_verify(),極大程度簡化了密碼操作流程,該函式會自動獲取隨機鹽並進行雜湊。

預防暴力破解和嘗試

只要時間足夠,暴力破解和嘗試總會得到一個正確的結果。對於此,我們可以限制嘗試的頻率和次數,或者封鎖敏感 IP。

升級遺留系統

對於那些使用明文或採用不安全的雜湊方法儲存密碼的遺留系統,升級它們的方式大致分為以下兩種。

在每個使用者登入的時候使用新的雜湊函式升級密碼

如果使用者該次登入的密碼匹配於資料庫的密碼,則可以用當前密碼值重新雜湊,並替換掉資料庫的密碼。但這種被動的替換方式可能會持續很長時間(需要使用者自行觸發),所以資料庫需要有一個標識欄位,用以表示該密碼是否已經置換成功。但給資料表新增欄位並不容易,尤其是對於執行中的大型應用。

在原密碼基礎上再雜湊

採用這種方式,可以選擇一個時機,統一對使用者密碼欄位進行遍歷更新。看上去是一勞永逸的方法,但會使得密碼驗證機制效率變低。而且,現有系統會一直被早先的機制所拖累。

身份驗證與許可權控制

身份與許可權

確保訪問的頁面、參與的業務請求都必須被身份驗證和許可權控制模組所覆蓋。警惕重定向導致的許可權穿透。

模糊處理

很多資料表中使用自增主鍵作為記錄的唯一標識。並且在 Cookie 和 API 中使用這些整形值。這會造成一些隱患,建議的做法是將這些值混淆到一些字串中,使得它們被模糊處理。

安全的檔案操作

一些框架中會使用某個路徑作為公開資料夾,比如 /public,這也就意味著我們可以通過對該資料夾的相對路徑直接訪問到其中的檔案,而無視許可權和身份的限制。建議的做法是將敏感的、需要安全防護的檔案放置在其他路徑中,使得通過 URL 無法直接訪問。

預設安全及跨站攻擊

預設安全

我們應該為驗證邏輯提供預設值,以保證在沒有考慮全面之時不會引發大型漏洞。

此外,不要相信動態型別,尤其是在判斷語句中,整形返回值和布林值的隱式轉換可能會造成嚴重的後果。

XSS 與 CSRF

XSS(跨站指令碼攻擊)和 CSRF(跨站請求偽造)分別是使用者過分信任網站與網站過分信任瀏覽器所產生的安全隱患。前者的解決方案通常是在輸入和輸出時進行檢測和過濾,而後者通常是在提交表單中新增 token 令牌。關於這兩種攻擊的細節可以參見 參考連結

多次表單提交

這裡涉及到 API 中的冪等性問題,指的是一次和多次對某一個資源的請求應該具有同樣的副作用。基於此,建立資料的請求是不符合冪等性的。比如由於網路延遲問題,使用者多次點選建立按鈕,傳送的合法建立請求先後抵達伺服器,從而導致建立行為產生多次。這一問題在轉賬等業務上也比較普遍。具體可以參考 「理解HTTP冪等性」。使用之前提到的一次性的 token 令牌可以預防這一問題的出現。

條件競爭

應對併發情況,需要考慮對檔案、資料庫等資源的併發處理策略。必要時需要對操作的檔案加鎖,以及對資料庫使用 ... for update 以新增悲觀鎖或通過版本欄位實現樂觀鎖。可以參見 參考連結

相關文章