書名:建構安全的 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
此時,災難就發生了。
注:
--
表示注釋後續的語句。注意
--
後有一個空格。這裡使用
#
也可以達到注釋的效果。
解決方法
- 在執行前進行敏感字元的過濾(不能隻通過 JavaScript);
- 為不同的業務子產品配置設定細顆粒度權限的資料庫連接配接;
- 使用預處理和占位符,如
以及$db->prepare()
;$db->execute()
- 使用存儲過程。但這種做法将部分業務邏輯轉移到了資料庫層,增加了測試和版本控制的難度。
批量指派陷阱
使用
$_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
身份。
解決方法
- 字段映射。資料庫字段、資料庫視圖字段、API 接口字段不完全相同,使得攻擊者難以知曉資料庫字段的真實名;
- 給可以被安全指派的字段加上白名單、或給危險字段加黑名單。如 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" target="_blank" rel="external nofollow" >
這時,當通路的 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
以添加悲觀鎖或通過版本字段實作樂觀鎖。可以參見 參考連結。