注意:可能使您的網站不安全的 PHP 和 WordPress 函數
已發表: 2022-03-10WordPress(或任何)網站的安全性是一個多方面的問題。 任何人都可以採取的確保網站安全的最重要步驟是記住,沒有任何單一的流程或方法足以確保不會發生任何不良事件。 但是您可以做一些事情來提供幫助。 其中之一是在您編寫的代碼和您部署的其他人的代碼中密切關注可能產生負面後果的功能。 在本文中,我們將準確介紹這些功能:WordPress 開發人員在使用之前應該清楚地考慮的功能。
WordPress 本身提供了一個龐大的函數庫,其中一些可能很危險。 除此之外,WordPress (PHP) 開發人員還會以某些頻率使用許多 PHP 函數,這些函數在使用時可能很危險。
所有這些函數都有合法和安全的用途,但它們也是可以使您的代碼很容易被濫用的函數。 我們將介紹最有可能導致安全漏洞的因素,以及您必須注意它們的原因。 我們將首先在您的代碼中運行不良行為者可以用來作惡的 PHP 函數,然後討論也會引起頭痛的 WordPress 特定的 PHP 函數。
需要注意的 PHP 函數
正如我們所說,PHP 包含許多易於使用的函數。 其中一些功能因人們很容易用它們做壞事而臭名昭著。 所有這些函數都有適當的用途,但要小心你如何使用它們以及你拉入項目的其他代碼是如何做的。
如果您的 PHP 版本支持它們,我們將假設您已經關閉了魔術引號和註冊全局變量。 它們在 PHP 5 及更高版本中默認關閉,但可以在低於 5.4 的版本中打開。 很少有主機允許它,但我認為沒有包含它們的 PHP 安全文章是不完整的。
或者提到 PHP 5.6 是最後一個仍然具有持續安全支持的 5.x 版本。 至少你應該在上面。 您應該計劃在 2018 年底 5.6 支持結束之前遷移到 PHP 7。
之後,您將需要處理一些有風險的功能。 從...開始…
extract
是壞消息,尤其是在$_POST
或類似的
幸運的是,使用 PHP 的extract
函數在很大程度上已經失寵。 它使用的核心是您可以在一組數據上運行它,並且它的鍵值對將成為您代碼中的動態變量。 所以
$arr = array( 'red' => 5 ); extract($arr); echo $red; // 5
會工作。 這很酷,但是如果您要提取$_GET
、 $_POST
等,這也非常危險。在這些情況下,您實際上是在自己重新創建register_globals
問題:外部攻擊者可以通過添加查詢字符串輕鬆更改您的變量值或表單域。 最好的解決方案很簡單:不要使用extract
。
如果您創建自己提取的數組,則使用extract
並不是特別安全問題,在某些情況下,它可能很有用。 但它的所有用途都有讓未來讀者感到困惑的問題。 創建一個數組並調用extract
比僅僅聲明你的變量更令人困惑。 所以我鼓勵你改為手動聲明,除非它完全不可行。
如果必須對用戶輸入使用extract
,則應始終使用EXTR_SKIP
標誌。 這仍然存在混淆問題,但它消除了惡意外來者通過簡單的查詢字符串或 Web 表單修改來更改您的預設值的可能性。 (因為它“跳過”了已經設置的值。)
“ eval
是邪惡的”,因為任意代碼很可怕
PHP 中的eval
以及幾乎任何其他帶有它的語言總是在這樣的列表中名列前茅。 並且有充分的理由。 PHP 在 PHP.net 上的官方文檔非常坦率地說:
注意: eval() 語言結構非常危險,因為它允許執行任意 PHP 代碼。 因此不鼓勵使用它。 如果您已仔細驗證除了使用此構造之外別無選擇,請特別注意不要將任何用戶提供的數據傳入其中,而無需事先正確驗證。
eval
讓程序中的任意字符串都可以像 PHP 代碼一樣運行。 這意味著它對於“元編程”很有用,您正在構建一個可以自己構建程序的程序。 這也是非常危險的,因為如果您允許任意源(例如網頁上的文本框)立即傳遞給您的eval
字符串評估器,那麼您突然讓惡意攻擊者可以輕鬆地執行 PHP 可以做的任何事情在你的服務器上做。 很明顯,這包括連接到數據庫、刪除文件以及通過 SSH 連接到機器時可以做的任何其他事情。 這是不好的。
如果你必須在你的程序中使用eval
,你應該竭盡全力確保你不允許任意的用戶輸入被傳遞給它。 如果您必須允許任意用戶訪問eval
的輸入,請通過您不讓他們運行的命令的黑名單來限制他們可以做什麼,或者(更好,但更難實現)白名單,這只是您的命令考慮安全。 更好的是,只允許少量特定的參數更改,例如僅經過驗證的整數。
但請始終牢記 PHP 創始人 Rasmus Lerdorf 的這句話:
“如果 `eval()` 是答案,那你幾乎肯定是問錯了問題。”
eval
變化
除了眾所周知的eval
之外,PHP 歷史上還有多種其他方式支持字符串求值作為代碼。 與 WordPress 開發人員最相關的兩個是帶有/e
修飾符的preg_replace
和create_function
。 每個都有點不同,PHP 5.5.0 之後的preg_replace
根本不起作用。 (PHP 5.5 及以下版本不再獲得官方安全更新,因此最好不要使用。)
/e
正則表達式中的修飾符也是“邪惡的”
如果您運行的是 PHP 5.4.x 或更低版本,則需要留意以 e 結尾的 PHP preg_replace
調用。 這可能看起來像:
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
PHP 提供了多種在正則表達式中“隔離”的方法,但您要注意的核心是“e”作為preg_replace
第一個參數的最後一個字符。 如果它在那裡,您實際上是將整個找到的段作為參數傳遞給您的內聯 PHP,然後對其進行eval
。 如果您讓用戶輸入進入您的函數,這與eval
具有完全相同的問題。 可以使用preg_replace_callback
替換示例代碼。 這樣做的好處是你已經寫出了你的函數,所以攻擊者更難改變被評估的內容。 所以你可以把上面寫成:
// uppercase headings $html = preg_replace_callback( '( (.*?) )', function ($m) { return " " . strtoupper($m[2]) . " "; }, $html );
// uppercase headings $html = preg_replace_callback( '( (.*?) )', function ($m) { return " " . strtoupper($m[2]) . " "; }, $html );
// uppercase headings $html = preg_replace_callback( '( (.*?) )', function ($m) { return " " . strtoupper($m[2]) . " "; }, $html );
// uppercase headings $html = preg_replace_callback( '( (.*?) )', function ($m) { return " " . strtoupper($m[2]) . " "; }, $html );
// uppercase headings $html = preg_replace_callback( '( (.*?) )', function ($m) { return " " . strtoupper($m[2]) . " "; }, $html );
讓用戶輸入create_function
好……
PHP 也有create_function
函數。 現在在 PHP 7.2 中已棄用,但它與eval
非常相似,並且具有相同的基本缺點:它允許將字符串(第二個參數)轉換為可執行的 PHP。 它也有同樣的風險:如果你不小心的話,你很容易不小心讓智能破解者在你的服務器上做任何事情。
如果您使用的是 5.3 以上的 PHP,則此問題比preg_replace
更容易修復。 您可以創建自己的匿名函數,而無需使用字符串作為中介。 至少在我看來,這既更安全也更易讀。
assert
Is Also eval
-Like
assert
不是我看到許多 PHP 開發人員在 WordPress 或外部使用的函數。 它的目的是針對您的代碼的先決條件進行非常輕量級的斷言。 但它也支持eval
類型的操作。 出於這個原因,你應該像對eval
一樣警惕它。 基於字符串的斷言(這是為什麼不好的核心)在 PHP 7.2 中也被棄用了,這意味著它在未來應該不再受到關注。
包含變量文件是一種可能允許不受控制的 PHP 執行的方法
我們已經很好地探討了為什麼eval
不好,但是像include
或require($filename'.php')
這樣的東西,特別是在$filename
是從用戶可控值設置的地方,同樣是壞消息。 原因與eval
略有不同。 可變文件名通常用於非 WordPress PHP 應用程序中的簡單 URL 到文件路由等。 但您也可能會在 WordPress 中看到它們。
問題的核心是,當您include
或require
(或include_once
或require_once
)時,您正在使腳本執行包含的文件。 它或多或少在概念上對那個文件進行了eval
,儘管我們很少這樣想。
如果您已經編寫了變量include
可能引入的所有文件,並考慮了這樣做時會發生什麼,那麼您很好。 但是,如果您沒有考慮過include
password.php
.php 或wp-config.php
會做什麼,那就是個壞消息。 如果有人可以添加惡意文件然後執行您的include
,這也是一個壞消息(儘管此時您可能會遇到更大的問題)。
不過,解決這個問題的方法並不難:硬編碼盡可能包含在內。 如果不能,請擁有一個白名單(更好)或一個可以包含的文件的黑名單。 如果白名單中的文件(即:您已經審核了添加時的作用),您就會知道自己是安全的。 如果它不在您的白名單上,您的腳本將不會包含它。 通過這個簡單的調整,你就很安全了。 白名單看起來像這樣:
$white_list = ['db.php', filter.php', 'condense.php'] If (in_array($white_list, $file_to_include)) { include($file_to_include); }
永遠不要將用戶輸入傳遞給shell_exec
和變體
這是一個很大的。 PHP 中的shell_exec
、 system
、 exec
和反引號都允許您正在運行的代碼與底層(通常是 Unix)shell 對話。 這類似於使eval
危險但加倍的原因。 加倍是因為如果你不小心讓用戶輸入通過這裡,攻擊者甚至不受 PHP 的約束。
從 PHP 運行 shell 命令的能力對於開發人員來說非常有用。 但如果你讓用戶的輸入在那裡,他們就有能力偷偷地獲得許多危險的權力。 所以我什至會說用戶輸入永遠不應該傳遞給shell_exec
類型的函數。
如果您想實現它,我認為處理這種情況的最佳方法是為用戶提供對一小組已知安全的預定義 shell 命令的訪問權限。 這可能是安全的。 但即便如此,我還是會警告你要非常小心。
注意unserialize
; 它自動運行代碼
在一個活的 PHP 對像上調用serialize
的核心操作,將該數據存儲在某個地方,然後使用該存儲的值稍後將該對象反unserialize
,這很酷。 這也是相當普遍的,但它可能是有風險的。 為什麼有風險? 如果該unserialize
調用的輸入不是完全安全的(比如它被存儲為 cookie 而不是在您的數據庫中……),攻擊者可以改變對象的內部狀態,從而使unserialize
調用做壞事。
與eval
問題相比,此漏洞利用更加深奧且不太可能被注意到。 但是,如果您使用 cookie 作為序列化數據的存儲機制,請不要對該數據使用serialize
。 使用類似json_encode
和json_decode
的東西。 有了這兩個 PHP 將永遠不會自動執行任何代碼。
這裡的核心漏洞是當unserialize
將 sa 字符串反序列化為一個類時,它會在該類上調用魔術__wakeup
方法。 如果允許對未經驗證的用戶輸入進行反unserialized
,則__wakeup
方法中的數據庫調用或文件刪除等操作可能會指向危險或不受歡迎的位置。
unserialize
與eval
漏洞不同,因為它需要對對象使用魔術方法。 攻擊者不是創建自己的代碼,而是被迫在對像上濫用您已經編寫的方法。 對像上的__destruct
和__toString
魔術方法也是有風險的,正如這個 OWASP Wiki 頁面所解釋的那樣。
通常,如果您不在類中使用__wakeup
、 __destruct
或__toString
方法,就可以了。 但是因為你以後可能會看到有人將它們添加到一個類中,所以最好不要讓用戶靠近你的調用來serialize
和反序列化,並通過像 JSON( json_encode
和unserialize
)這樣的東西傳遞所有公共數據以供這種使用json_decode
永遠不會自動執行代碼。
使用file_get_contents
獲取 URL 是有風險的
快速編寫一些必須調用外部 URL 的 PHP 代碼時,一種常見的做法是訪問file_get_contents
。 它很快,很容易,但不是超級安全。
file_get_contents
的問題很微妙,但很常見的是主機有時會將 PHP 配置為甚至不允許您訪問外部 URL。 這是為了保護你。
這裡的問題是file_get_contents
將為您獲取遠程頁面。 但是當它這樣做時,它不會檢查 HTTPS 協議連接的完整性。 這意味著您的腳本可能成為中間人攻擊的受害者,這將允許攻擊者將他們想要的任何內容放入您的file_get_contents
頁面結果中。
這是更深奧的攻擊。 但是當我編寫現代(基於 Composer)的 PHP 時,為了防止它發生,我幾乎總是使用 Guzzle 來包裝更安全的 cURL API。 在 WordPress 中,它更容易:使用wp_remote_get
。 它的工作方式比file_get_contents
更一致,並且默認驗證 SSL 連接。 (您可以將其關閉,但是,嗯,也許不要……)更好,但更令人討厭的是wp_safe_remote_get
等。這些功能與名稱中沒有safe_
的功能相同,但它們會使確保沿途不會發生不安全的重定向和轉發。
不要盲目相信來自filter_var
URL 驗證
所以這個有點晦澀,所以感謝 Chris Weigman 在這個 WordCamp 演講中解釋它。 一般來說,PHP 的filter_var
是驗證或清理數據的好方法。 (雖然,不要對你想要做什麼感到困惑......)
這裡的問題非常具體:如果您嘗試使用它來確保 URL 是安全的,則filter_var
不會驗證協議。 這通常不是問題,但如果您將用戶輸入傳遞給此方法進行驗證,並且正在使用FILTER_VALIDATE_URL
,則類似javascript://comment%0aalert(1)
的內容將通過。 也就是說,這可能是一個非常好的載體,可以在你意想不到的地方進行基本的 XSS 攻擊。
對於驗證 URL,WordPress 的esc_url
函數將產生類似的影響,但只允許通過允許的協議。 javascript
不在默認列表中,因此可以確保您的安全。 但是,與filter_var
不同的是,它會為傳遞給它的不允許的協議返回一個空字符串(不是 false)。
值得關注的 WordPress 特定功能
除了核心 PHP 可能存在漏洞的函數之外,還有一些 WordPress 特定的函數可能有點棘手。 其中一些與上面列出的各種危險函數非常相似,有些則略有不同。
WordPress 使用maybe_unserialize
如果您閱讀以上內容,這可能很明顯。 在 WordPress 中有一個名為maybe_unserialize
的函數,正如您猜想的那樣,如果需要,它會反序列化傳遞給它的內容。
這並沒有引入任何新的漏洞,問題很簡單,就像核心的反unserialize
函數一樣,這個漏洞可能導致易受攻擊的對像在反序列化時被利用。
如果用戶是管理員, is_admin
不回答!
這個很簡單,但功能名稱含糊不清,所以如果你趕時間,很容易混淆或錯過。 您應該始終檢查嘗試在 WordPress 中執行操作的用戶是否具有執行該操作所需的權利和特權。 為此,您應該使用current_user_can
函數。
但是您可能會錯誤地認為is_admin
會告訴您當前用戶是否是管理員級別的帳戶,因此應該能夠設置插件使用的選項。 這是個錯誤。 is_admin
在 WordPress 中所做的是告訴您當前頁面加載是否在站點的管理端(相對於前端)。 因此,每個可以訪問管理頁面(如“儀表板”)的用戶都可能通過此檢查。 只要您記住is_admin
是關於頁麵類型,而不是當前用戶,就可以了。
add_query_arg()
不清理 URL
這並不常見,但幾年前 WordPress 生態系統出現了一波更新浪潮,因為有關此功能的公共文檔不正確。 核心問題是add_query_arg
函數(及其反向remove_query_arg
)不會自動清理站點 URL,如果 URL 沒有傳遞給它,人們認為它確實如此。 許多插件被 Codex 誤導,因此使用它不安全。
他們必須做不同的核心事情:在使用之前清理調用此函數的結果。 如果你這樣做了,你就真的可以免受你認為的 XSS 攻擊了。 所以看起來像:
echo esc_url( add_query_arg( 'foo', 'bar' ) );
$wpdb->query()
對 SQL 注入攻擊開放
如果您了解 SQL 注入,這可能看起來很愚蠢,甚至是多餘的。 因為問題是您訪問數據庫的任何方式(例如使用 PHP 的mysqli
或 PDO 數據庫驅動程序)來創建允許 SQL 注入攻擊的數據庫查詢。
我特別調用$wpdb->query
的原因是$wpdb
wpdb 上的一些其他方法(如insert
、 delete
等)會為您處理注入攻擊。 此外,如果您習慣於使用WP_Query
或類似工具進行基本的 WordPress 數據庫查詢,則無需考慮 SQL 注入。 這就是為什麼我要大聲疾呼:當您第一次嘗試使用$wpdb
進行自己的查詢時,以確保您了解對數據庫的注入攻擊是可能的。
該怎麼辦? 使用$wpdb->prepare()
然後$wpdb->query()
。 您還需要確保在$wpdb
的其他“獲取”方法之前做好準備,例如get_row()
和get_var()
。 否則 Bobby Tables 可能會得到你。
esc_sql
也不能保護您免受 SQL 注入
對於大多數 WordPress 開發人員來說,我會說esc_sql
註冊沒有任何意義,但它應該註冊。 正如我們剛剛提到的,在進行任何數據庫查詢之前,您應該使用wpdb->prepare()
。 這會讓你安全。 但是,開發人員可能會使用esc_sql
來代替,這是很誘人且可以理解的。 他們可能認為它是安全的。
問題是esc_sql
沒有針對 SQL 注入的強大保護。 它確實是 PHP 的add_slashes
函數的美化版本,多年來一直不鼓勵您使用它來保護您的數據庫。
還有更多工作要做,但這是一個重要的開始
安全性不僅僅是在代碼中尋找攻擊者可能濫用的功能。 例如,我們沒有深入討論驗證和清理您從用戶那裡收到的所有數據並在將其放入網頁之前將其轉義的必要性(儘管我最近發表了一篇關於該主題的文章,“保護您的 WordPress站點對抗跨站點腳本攻擊”)。 但是您可以並且應該將其用作更廣泛的安全策略的一部分。
在您在 WordPress 中部署新插件之前進行最終檢查時,將易於濫用的功能列表放在您身邊是一個好主意。 但是您還需要確保您信任主機的安全性,確保您的用戶擁有正確的密碼等等。
關於安全性要記住的核心問題是,最薄弱的環節才是最重要的。 一個領域的良好做法會加強其他地方可能存在的弱點,但它們永遠無法完全糾正它。 但要注意這些功能,你會在大多數情況下占得先機。