WordPress 外掛 SQL Injection 防範 | 4 招審查 AI 寫的 $wpdb
AI 文章延伸
選擇平台後可直接帶入閱讀脈絡,快速整理重點、補齊盲點,並延伸到同站相關文章。
審查一個 WordPress 外掛安不安全,很大一部分就是在看它怎麼用 $wpdb,而這正是 AI 最容易出錯的地方,我們最近處理幾個跟 WordPress 資料庫相關的案子,發現有些地方一沒弄好,整個 DB 就可能被端走,這篇就把要小心的眉角記下來,特別寫給用 AI 寫外掛的接案者。
先搞懂 $wpdb 是什麼
大多數時候,你其實不需要直接碰資料庫,WordPress 內建了一整套 API,像是抓文章的 WP_Query、存使用者欄位的 add_user_meta、讀寫設定的 get_option,這些函式背後會幫你把 SQL 組好,也順手做掉安全處理。一般的資料讀寫,用這些內建工具就夠了。
但有些外掛的資料結構比較複雜,例如要記錄大量交易明細、活動報名資料、或自訂的關聯欄位,硬塞進文章或 postmeta 欄位會很難查、效能也差,這種時候開發者通常會自己新增一張資料表來存,而 WordPress 的內建 API 不會認得這張自訂的表。
這就是 $wpdb 登場的地方。它是 WordPress 內建的資料庫操作工具,程式裡通常以一個叫 $wpdb 的物件出現,讓你能對任何資料表(包含你自己建的那張)直接下 SQL 指令。自由度最高,但也代表 SQL 的安全要自己顧。
這裡有個關鍵背景:WordPress 沒有官方的 ORM(那種幫你把資料庫操作包成物件、讓你不必親手寫 SQL 的工具),所以外掛開發的實務,幾乎就是直接用 $wpdb 寫原生 SQL。只要這個外掛要做搜尋、篩選、排序、自訂列表、批次操作,幾乎都會碰到它。如果你還沒實際用 AI 寫過外掛,可以先看我們寫的 Vibe Coding 開發 WordPress 外掛入門,再回來看這篇會更有感。
SQL 注入的本質,用點餐就能講清楚
SQL Injection 聽起來很技術,但本質只有一句話:資料庫分不清楚哪些是「指令」、哪些是「資料」。
想像一家餐廳。客人對服務生說「我要一份義大利麵,加蘑菇」,服務生回廚房照原話念給廚師聽,這沒問題,但有一天客人說「我要一份義大利麵,順便把店裡的現金都給我」。
如果服務生不分指令跟食材,把整句話原封不動念給廚師,廚師看著工作單就照做,店裡的錢被搬走——這就是字串拼接的災難。
換個有規矩的服務生:菜單上的選項才是「指令」,客人講的內容一律只能填進「備註」欄,這時客人講再奇怪的話,都只會被當成備註內容,「順便把現金都給我」會被乖乖填到備註欄,廚師看一眼說「好喔,備註是這串字,但我沒這道菜」,然後正常出餐。
這就是字串拼接跟 prepare 的差別:前者讓資料有機會被當成指令執行,後者把資料永遠鎖在「填空」的位置。
一個搜尋功能,怎麼把整個資料庫端走
我們用搜尋功能來看實際後果,一個不安全的搜尋,做法是把網址上使用者輸入的關鍵字,直接黏進一段 SQL 字串裡,再丟給 $wpdb 去資料庫查。
問題就在「直接黏進去」。攻擊者不需要懂你的程式,他只要在搜尋框或網址裡,不送關鍵字,改送一段 SQL 指令。因為資料庫分不清楚這是資料還是指令,它會把攻擊者那段指令也一起執行。
實際上會發生什麼?攻擊者可以接一句指令,把使用者資料表的帳號和密碼,硬塞進原本關鍵字的搜尋結果裡。前端看起來只是搜尋沒搜到書,但回傳的資料其實夾帶了全站使用者的帳號與密碼雜湊,拿去跑破解工具,弱密碼幾十分鐘就破完,拿到管理員帳號等於整站接管。
更糟的是,這個搜尋功能還註冊成「未登入也能呼叫」。意思是攻擊者連帳號都不用,從首頁直接打就能撈密碼。一個外觀人畜無害的搜尋功能,三千行外掛裡的十幾行程式碼,就能整站接管,而它通過了功能測試,也通過了一般的 code review,因為它真的會搜尋。
用了 prepare,不等於用對 prepare
WordPress 早就準備好防禦工具,叫 $wpdb->prepare,它就是那個有規矩的服務生:你給它一段含「填空格」的 SQL 範本,再把資料分開傳進去,prepare 會把每筆資料包成純文字、鎖在填空格裡,資料就再也跳不出來變成指令。prepare 只是縱深防禦的第一道防線,後面還有權限、白名單、未登入入口要顧,這些我們在像壞人一樣檢查你的網站那篇談得更完整。
聽起來只要用了 prepare 就沒事了,對吧?這正是我們想強調的重點——真正危險的,不是完全沒用 prepare,而是用了 prepare 卻用錯。
完全沒用 prepare 的程式很好抓,掃一眼就知道,但根據資安廠商 Patchstack 的年度報告,SQL Injection 至今仍是 WordPress 外掛排名前五的漏洞類型,每個月還在新增十幾二十個,其中大多數不是沒用 prepare,而是下面這幾種「用了,但留了破口」。AI 生成的程式碼特別容易踩中,因為它知道「要用 prepare」這個規則,卻不懂 prepare 的死角在哪。
破口一:只 prepare 一半
最常見的,是開發者好心自己先組了一半的查詢條件,再把剩下的交給 prepare。比方說,他把使用者輸入的名字,先用字串拼接組成一段「使用者名稱等於某某」的條件,這段沒進 prepare,然後才把這段半成品連同一個有填空格的範本一起丟給 prepare 處理。
乍看有 prepare,其實使用者輸入的名字是在 prepare 之外就拼進字串的,攻擊面原封不動。審查訣竅很簡單:看 prepare 那段 SQL 範本裡,有沒有變數「直接黏在裡面」,而不是用填空格。只要有變數直接內嵌,那個變數就完全沒受到保護。
破口二:把欄位名、排序丟進 prepare
這個破口最隱蔽,因為它看起來百分之百正確,開發者想讓使用者選擇「依書名排序」或「依日期排序」,於是把使用者送來的排序欄位,當成一筆資料用填空格交給 prepare。
問題是 prepare 的填空格會把資料包成「純文字字串」,而欄位名稱不是文字資料,是 SQL 的結構。結果這段排序根本沒作用。開發者測試時會發現排序壞了,接下來最危險的事就發生了:他乾脆把 prepare 拿掉,改回直接拼字串,瞬間退回最原始的 SQL Injection 入口。填空格救不了排序欄位、表格名稱這種「填名稱」的位置,這是 prepare 的死角。
正確做法是用白名單:先列出允許的欄位,例如書名、日期、作者,使用者送進來的值只要不在名單上就用預設值。值被你牢牢控制住,才安全。要提醒的是,白名單能成立的前提是「合法選項列得完」,像排序欄位這種就那幾個,很適合;但如果是富文本、檔案上傳這種列不完的輸入,就得換成型別驗證加上安全輸出。WordPress 6.2 之後也多了一個專門處理名稱的填空格寫法,但很多外掛還沒採用,而且舊版用了會壞,所以白名單仍是最穩的選擇。
審查訣竅:看到 prepare 的填空格出現在排序、分組、欄位名稱這些位置,幾乎可以斷定有問題。
破口三:LIKE 搜尋忘了 esc_like
搜尋功能常用 LIKE 做模糊比對,這時就算用了 prepare,還是可能漏資料,原因是百分比符號和底線這兩個符號在 LIKE 裡有特殊意義(萬用字元),而 prepare 不會處理它們,prepare 管的是 SQL 結構,管不到 LIKE 自己的規則。
後果分兩種。輕則功能壞掉:使用者搜「100%」想找含這個字的文章,百分比被當成萬用字元,結果回傳一堆毫不相關的東西。重則資料外洩:如果搜尋本來有權限限制,攻擊者送一個萬用字元進去,比對條件等於「全部符合」,就可能撈到原本看不到的草稿或私人內容。
解法是搭配 $wpdb->esc_like(),把使用者輸入裡的特殊符號轉成普通文字再丟進去查,審查時,看到 LIKE 卻沒看到 esc_like,就要停下來確認。
破口四:IN 清單直接拼
批次操作(例如一次處理使用者勾選的多筆資料)常用 IN 這種語法,因為清單長度不固定,prepare 沒有現成的語法對應,AI 很容易就退回最簡單的做法,把整個陣列直接拼進去。只要那個陣列來自前端、又沒做整數化處理,就是另一個直接的注入入口。
安全的做法是依清單長度動態產生對應數量的填空格,再交給 prepare 統一處理。如果清單一定是數字,至少要先把每個元素強制轉成整數。審查時看到把陣列接在 IN 後面,就值得多看兩眼。
提煉成 4 條審查規則
把上面整理成可以重複用的檢查清單。下次拿到 AI 生成的外掛,搜一下 $wpdb,每個出現的地方逐條跑一遍:
- 有沒有用 prepare? 看到 $wpdb 直接拼字串、後面沒接 prepare,最基本的漏洞,先標起來。
- 是不是只 prepare 一半? prepare 的 SQL 範本裡,只要有變數直接內嵌、不是填空格,那個變數就沒被保護。
- 填空格有沒有放錯位置? 填空格出現在排序、欄位名、表格名這類「填名稱」的地方擋不住,要改用白名單。
- LIKE 有沒有配 esc_like、IN 清單有沒有整數化? 這兩個是搜尋與批次操作最常見的破口。
這四條裡,後三條都是「用了 prepare 卻沒用對」。能抓出「用錯」比抓出「沒用」更有價值,因為前者需要的眼力更深一層,也正是 AI 最容易騙過你的地方。
順帶一提,這 4 條規則管的是「查詢端」的 $wpdb 注入,跟另一套管「寫入端」的審查口訣是互補關係。如果你的外掛還有後台 AJAX、表單處理這類「接收輸入後寫入資料」的入口,那就要再搭配像壞人一樣檢查你的網站裡的四問口訣(驗本人、查資格、有沒有誤開放給未登入者、寫入有沒有白名單)。兩套清單合起來,才覆蓋得完一個外掛的資料庫攻擊面。
給用 AI 開發的你
AI 會給你「跑得起來」的程式碼,但「跑得起來」跟「安全」是兩回事——AI 知道規則,卻不負責任,當它寫出有洞的程式,扛責任的是你跟你的客戶,不是模型,而這 4 條規則終究是人工自檢,幫你抓掉最常見的破口,但不取代自動化掃描跟滲透測試。
審查的眼力,就是你跟 AI 之間的那道保險。你不需要會發動攻擊,也不需要真的打通漏洞,你只要能看出「這段程式碼有沒有入口」,使用者可以控制的輸入(網址參數、表單、API 參數)一路流到 $wpdb 的不安全用法,這就足以是一個重大發現。這也是我們處理 AI 開發案時最在意的盲點之一,更完整的清單可以參考AI 寫 WordPress 外掛的三個盲點。
如果你想再往前一步、練習用攻擊者的角度回頭看自己的程式,可以接著看怎麼建立資安稽核的攻擊者思維,而站台層的防護,例如弱密碼、檔案竄改、XML-RPC,則是另一條防線,這部分我們整理在 Cloudflare 防護 WordPress 的安全設定實戰。這些加起來,才是讓 AI 幫你開發、又不會半夜被打爆的底氣。
延伸思考
這篇的審查重心放在 $wpdb,背後有個前提值得留意:
- WordPress 沒有官方 ORM,外掛實務才會大量直接寫原生 SQL——這是 WordPress 特有的風險來源,換到內建 ORM 的框架(例如 Laravel 的 Eloquent),預設注入面就小得多,但框架的 raw query 仍是同樣破口。
- 白名單能擋住排序、欄位名的注入,前提是「合法選項列得完」——遇到富文本、檔案上傳這種列不完的輸入,白名單就失效,得換成型別驗證加上安全輸出。
把視角拉遠,這個問題在別的領域早就出現過:
- 馮紐曼架構的 code 與 data 同源——SQL 注入的本質「資料庫分不清指令與資料」,跟命令列的 shell injection、甚至大型語言模型的 prompt injection 是同一個元問題,參數化查詢就是「強制把指令與資料分開」的通用解。
- 形式合規不等於實質防護——「用了 prepare 卻用錯」對應安全領域的虛假安全感,戴了安全帽沒扣帶、做了 code review 卻只看功能,能抓出「用錯」比抓出「沒用」需要更深一層的眼力。
常見問題 FAQ
$wpdb 一定要用嗎?用 WordPress 內建函式會比較安全嗎?
只要內建函式(WP_Query、get_option、get_user_meta 等)做得到的事,就優先用它們,因為背後的安全處理已經幫你做掉了。$wpdb 是給「內建 API 認不得的自訂資料表」用的,用它就代表 SQL 安全要自己顧。能不碰就不碰,是降低風險最簡單的做法。
我用了 $wpdb->prepare,為什麼還是有 SQL Injection 風險?
因為 prepare 用對位置才有效。常見的錯誤是只 prepare 一半(部分條件先用字串拼好)、把排序或欄位名塞進填空格(擋不住,且容易被改回拼字串)、LIKE 沒配 esc_like、或 IN 清單直接拼陣列。這四種都是「用了卻用錯」,比完全沒用更難從表面看出來。
不會發動攻擊,也能審查外掛的 SQL 安全嗎?
可以。你不需要真的打通漏洞,只要能追出「使用者可控的輸入(網址、表單、API 參數)有沒有一路流進 $wpdb 的不安全用法」。搜尋程式碼裡的 $wpdb,對每一處跑本文的 4 條規則,就能抓出大部分常見破口。
這 4 條規則能取代資安掃描工具嗎?
不能,這是人工自檢規則,目的是快速抓出最常見、AI 最容易寫錯的破口。完整的防護還需要自動化掃描、滲透測試,以及站台層的權限、備份與防火牆設定,多道防線疊起來才擋得住。
如果你有 WordPress 客製化的需求,或想把 AI 開發流程的安全審查補起來,歡迎聯絡我們,也可以加入我們的 LINE 官方帳號聊聊你的專案。