WordPress XSS 與 SQL 注入防護完整設定

跨站腳本(XSS)與 SQL 注入是 WordPress 網站長年被入侵的兩大途徑,而它們幾乎都不是 WordPress 核心的問題,而是出在主題、外掛,以及站長自己沒做好的幾個設定。攻擊者不需要猜你的密碼,只要找到一個沒做好過濾的留言欄、一個直接把網址參數塞進資料庫查詢的外掛,就能偷走後台 Cookie、撈出整張使用者資料表,甚至在你的頁面裡植入長期潛伏的惡意腳本。

這篇文章會把兩件事一次講清楚:第一,XSS 與 SQL 注入在 WordPress 上到底怎麼發生,為什麼一般網站最容易中招;第二,分成「會寫程式的開發者」與「只管後台的站長」兩種角色,各自該做哪些 WordPress XSS SQL注入防護的設定與寫法。看完你會知道哪一層是你能補的、哪一層該交給防火牆,以及如何判斷自己的網站是不是已經漏了某一道門。

XSS 與 SQL 注入在 WordPress 上是怎麼發生的

兩者的共通點是「把不該被信任的輸入,當成可執行的內容處理」。差別在於攻擊的目標不同:XSS 把惡意腳本送進「瀏覽器」執行,SQL 注入把惡意指令送進「資料庫」執行。

跨站腳本攻擊指的是攻擊者在網頁某個會回顯的欄位裡塞入 JavaScript,例如把 <script> 標籤寫進留言、暱稱、搜尋關鍵字或表單欄位。如果網站把這段內容原封不動輸出到頁面,瀏覽器就會把它當程式碼跑起來,進而竊取登入 Cookie、冒用身分送出請求,或把訪客導去釣魚頁。XSS 又分兩型:反射型是惡意腳本透過網址參數臨時帶入、即時反射回頁面;儲存型則是腳本被寫進資料庫長期保存,之後每一個瀏覽該頁的人都會中招,危害最大。

SQL 注入則是攻擊者在輸入裡夾帶 SQL 語法片段。當外掛或主題把使用者輸入直接串接進查詢字串,這段語法就會被資料庫一起執行。一個典型的危險寫法是把網址參數直接接進查詢:

$wpdb->query( "SELECT * FROM {$wpdb->posts} WHERE ID = " . $_GET['id'] );

攻擊者只要在 id 參數送出 5 UNION SELECT user_pass, user_email FROM wp_users WHERE ID=1 --,就能把後台帳號的密碼雜湊與信箱撈出來。實務上攻擊者會先用 100-SLEEP(5) 這類測試字串探測:如果頁面真的延遲五秒才載入,就代表注入點存在,接著才送出正式的撈資料語句。

WordPress 之所以是高風險目標,不是因為核心脆弱,而是因為生態龐大。一個站常常裝了十幾個外掛,其中總有幾個是多年沒更新、或當初寫得不夠謹慎的。資安公司 Sucuri 過去就揭露過大量知名外掛因為誤用 add_query_arg()remove_query_arg() 這兩個函式而帶有 XSS 漏洞——這兩個函式不會自動跳脫輸出,開發者以為安全就直接印出,結果開了後門。換句話說,你網站的安全等級,往往是由裝得最隨便的那一個外掛決定的。

一條最重要的防護原則:輸入時清洗、輸出時跳脫

如果只能記一句話,就記這句:進來的資料要清洗(sanitize),出去的資料要跳脫(escape),中間還要驗證(validate)。這三件事對應三個不同時機,缺一個就留一個破口。

清洗是資料剛進來的當下做的事,目的是把不該存在的字元或標籤去掉,再存進資料庫或拿去處理。跳脫則是資料要輸出到頁面之前做的事,目的是讓任何字元都只會被當成「文字」顯示,而不會被瀏覽器當成 HTML 或腳本執行。驗證是更嚴格的把關,檢查資料是否符合預期格式,例如信箱欄位是不是真的長得像信箱、數量欄位是不是大於零。

很多人以為清洗過就安全了,這是最常見的誤解。只清洗不跳脫,萬一資料庫裡早有一筆沒清乾淨的舊資料(可能來自匯入、直接改資料庫、或別段程式的 bug),輸出時照樣會觸發儲存型 XSS。反過來,只跳脫不清洗,雖然顯示安全,但資料庫裡會存進一堆格式錯誤的髒資料。所以正確的心態是「對任何來源的資料都不信任」,連從自己資料庫讀出來的也一樣,輸出前一律跳脫。

這條原則同時涵蓋了 XSS 與 SQL 注入:輸出跳脫擋的是 XSS,查詢參數化擋的是 SQL 注入,而清洗與驗證是兩者共用的第一道濾網。

開發者該怎麼寫:參數化查詢與輸出跳脫

如果你會改主題或寫外掛,防護的主動權就在你手上,重點在四個機制:查詢參數化、輸入清洗、輸出跳脫、權限與 nonce 驗證。

用 wpdb prepare 做查詢參數化,杜絕 SQL 注入

只要你繞過 WordPress 內建 API、自己寫 $wpdb 查詢,就一定要用 $wpdb->prepare() 包起來。它的運作方式類似 PHP 的 sprintf(),在查詢字串裡放佔位符,再把實際值綁定進去,綁定時自動完成跳脫。佔位符有四種:%d 給整數、%f 給浮點數、%s 給字串、%i 給資料表或欄位名稱(WordPress 6.2 之後才支援)。

前面那段危險查詢,正確寫法是:

$wpdb->get_results(
  $wpdb->prepare(
    "SELECT * FROM {$wpdb->posts} WHERE ID = %d",
    $_GET['id']
  )
);

有幾個實務上最常見的踩雷點要特別留意:

  • 整數欄位用 %d,不要用 %s%s 會把值包成字串,讓 WHERE ID = '5 OR 1=1' 變成字串比對而非整數比對,邏輯就歪了。
  • prepare() 之後絕對不要再串接變數:正確呼叫了 prepare(),卻在結果字串後面又接一段原始變數,等於把漏洞重新打開。
  • 不要對 prepare() 的輸出再做 stripslashes():那會把保護用的跳脫字元拆掉。
  • LIKE 查詢要先用 $wpdb->esc_like():否則攻擊者能用 % 萬用字元把模糊搜尋變成逐字撈表。
  • IN() 子句的佔位符要動態產生prepare() 不接受陣列,要先用 array_fill()implode() 組出對應數量的 %d,不要用字串內插硬塞。

另外要知道 prepare() 管不到的地方。它只能參數化「值」,管不到「結構」。最典型的就是 ORDER BY 後面的欄位名稱——排序欄位無法用佔位符,正確做法是用允許清單:先定義一組程式允許的欄位名稱,確認傳進來的值在清單裡,才嵌入查詢。排序方向(ASC/DESC)、欄位名、資料表名這些「結構位置」的輸入,都要走允許清單驗證,不能靠 prepare()

還有一個好消息是,$wpdb->insert()$wpdb->update()$wpdb->delete() 這三個方法不需要另外包 prepare(),因為它們內建用格式陣列(%s%d%f)處理跳脫;唯一要注意的是別省略格式陣列,省了會全部預設成字串,整數欄位可能型別不符。

清洗輸入、跳脫輸出各用對的函式

清洗和跳脫都要挑「對應情境」的函式,用錯等於沒用。清洗端的常用函式:純文字用 sanitize_text_field()、需要保留換行的多行文字用 sanitize_textarea_field()、信箱用 sanitize_email()、正整數用 absint()、網址(存入用)用 esc_url_raw()、需要保留部分 HTML 的內容用 wp_kses()wp_kses_post() 並明確指定允許的標籤。

要強調一個常見錯誤:不要在「存入資料庫前」就用 esc_attr() 之類的跳脫函式去清洗。跳脫是輸出端的工作,提前用會導致資料從資料庫讀出後被重複跳脫(double-escape),顯示出亂碼。記住順序是「存入前清洗、輸出前跳脫」。

輸出端則依輸出位置選函式:輸出到一般文字節點用 esc_html()、輸出到 HTML 屬性值用 esc_attr()、輸出網址用 esc_url()、輸出到 JavaScript 用 esc_js()。同一個值在不同位置要用不同函式,例如一個網址印在連結文字裡用 esc_html(),放進 href 屬性就要用 esc_url()

對於下拉選單、單選按鈕這種「值是固定幾個選項」的欄位,更乾脆的做法不是清洗,而是驗證:直接用 in_array() 檢查送進來的值在不在允許的陣列裡,不在就一律當空值,這比任何清洗函式都嚴密。

加上權限檢查與 nonce,補上 CSRF 這道缺口

參數化查詢和輸出跳脫處理的是「資料安全」,但還有一類攻擊是「請求偽造」:攻擊者誘騙已登入的管理員點一個連結,借用對方還活著的登入狀態送出惡意請求,這叫跨站請求偽造(CSRF),常和 XSS 串在一起放大災情。

擋這類攻擊要靠兩個機制。第一是權限檢查,任何敏感動作執行前先用 current_user_can() 確認目前使用者真的有權限,而且要檢查「剛好夠用」的權限,不要全部都用 manage_options——是編輯內容的動作就檢查 edit_posts 就好。第二是 nonce 驗證,表單用 wp_nonce_field() 產生、提交時用 wp_verify_nonce() 比對;AJAX 則用 check_ajax_referer()。要注意 WordPress 的 nonce 不是真正一次性的,預設 24 小時內有效,本質是「綁定特定動作與使用者工作階段的短期權杖」,用來確認這個請求真的來自你自己的表單,而不是外部網站偽造的。

完整的安全動作順序是固定的:先檢查權限、再驗證 nonce、接著清洗輸入、執行工作、最後跳脫輸出。每一個表單處理、AJAX 回呼、REST API 端點都套這套流程,寫成習慣就不會漏。

站長不會寫程式,能做哪些 XSS 與 SQL 注入防護

就算完全不碰程式碼,光靠後台操作與設定,也能擋掉絕大多數透過已知漏洞發動的攻擊。重點是減少破口數量、加一道流量過濾,並降低萬一被打中的損失。

第一優先是把更新做滿。前面說過大多數 WordPress 入侵是透過老舊外掛或主題的已知漏洞,所以核心、主題、外掛全部保持最新版,本身就是 CP 值最高的防護。同時要刪掉沒在用的外掛與主題——停用不等於刪除,留在伺服器上的程式碼一樣可能被掃描到漏洞並利用。裝外掛前先看更新頻率與最後更新日期,太久沒維護的別裝。

第二是裝一套含網站應用防火牆(WAF)的資安外掛,例如 Wordfence。WAF 會攔截所有進出網站的流量,依規則檢查並擋下 SQL 注入、XSS、暴力破解等常見攻擊的請求字串。以 Wordfence 為例,安裝後它會提示做「延伸防護」設定,這會修改 .htaccess.user.ini 檔案,讓防火牆在 WordPress 核心載入前就先啟動,過濾效率最高;設定前記得先把這兩個檔案手動備份。剛裝可以先選「學習模式」讓它熟悉你的網站運作,過一陣子再切到「啟用並保護」。

要對 WAF 有正確期待:它是一道緩衝,不是萬靈丹。WAF 規則可以被編碼或拆解請求繞過,它的價值在於替還沒修補的漏洞爭取時間,真正的修補還是得回到程式碼那一層。把它當成「補強」而不是「替代」。

第三是做好登入與帳號防護,順手降低被當跳板的機會:開啟兩階段驗證(2FA)、設定登入失敗鎖定(例如錯三次鎖六小時)、用 reCAPTCHA 擋機器人,並把自己的 IP 加進白名單避免把自己鎖在外面。這些雖然不直接擋 XSS 或 SQL 注入,但能大幅降低後台帳號被攻破後,攻擊者趁機植入儲存型惡意腳本的風險。

第四是定期掃描與監控。資安外掛通常能定時掃描全站,找出被竄改的檔案、過期外掛與惡意程式碼並通知你。共享主機資源有限的話,可以開「低資源掃描」模式拉長掃描時間、降低主機負載。

怎麼判斷網站可能已經被注入或植入腳本

與其等到出事,不如主動找出徵兆。XSS 與 SQL 注入被成功利用後,通常會留下幾個可觀察的跡象。

最常見的是頁面出現你沒放過的內容:陌生的彈出視窗、會自動跳轉到陌生網站的連結、原始碼裡多出來的 <script> 標籤或 iframe,這些往往是儲存型 XSS 被寫進資料庫的結果。資料庫層面則可能出現異常的新管理員帳號、文章內容被竄改、或資料表裡多出可疑的選項值。伺服器流量突然暴增、或日誌裡出現大量帶著奇怪 SQL 片段(像 UNION SELECTSLEEP()的網址請求,也是被探測或注入的訊號。

檢查工具方面,可以用線上掃描服務檢視網站是否被植入惡意碼,也可以用會比對已知漏洞資料庫的掃描工具(例如 WPScan)核對你裝的外掛清單裡有沒有正在使用含已知注入漏洞的版本——這類工具抓不到自寫程式的漏洞,但能在熱門外掛的漏洞被大規模利用前先示警。

開發階段還有一個容易忽略的資訊外洩風險要堵:正式站的 wp-config.phpWP_DEBUG 必須設為 false。開著的話,一旦注入嘗試觸發資料庫錯誤,錯誤訊息會把資料表結構、欄位名稱直接印在頁面上,等於把網站的內部結構送給攻擊者。需要記錄錯誤時,改用 WP_DEBUG_LOG 寫進不對外開放的日誌檔,並把 WP_DEBUG_DISPLAY 設為 false

把防護想成多層的閘門,而不是單一開關

XSS 與 SQL 注入沒有「裝一個外掛就一勞永逸」的解法,真正有效的是把防護拆成一層層的閘門,讓攻擊者每一層都得突破。輸入層靠權限檢查、nonce 與清洗擋下未授權請求;查詢層靠 $wpdb->prepare() 與允許清單擋下 SQL 注入;輸出層靠 esc_html() 等跳脫函式擋下 XSS;傳輸層靠 HTTPS 與內容安全政策(CSP)等安全標頭,讓萬一漏進來的腳本也無法執行;伺服器層則靠降低資料庫帳號權限、鎖好檔案權限來縮小被打中後的損失範圍。攻擊者找的永遠是那道沒鎖或設錯的閘門,而不是已經鎖好的。

所以接下來該做的,是先依角色對號入座:你會寫程式,就回頭檢查自己的主題與外掛有沒有原始查詢、有沒有輸出沒跳脫的地方,優先審用戶註冊、結帳、個人資料這幾個高價值的入口;你只管後台,就把更新做滿、裝好含 WAF 的資安外掛、開啟 2FA 與登入鎖定,並排定期掃描。把每一層的閘門逐一補上、逐一確認真的鎖好,網站的安全就不再是賭運氣,而是有把握的事。

相關文章
標籤: SQL 注入, wpdb prepare, WAF, WordPress 資安, XSS