WordPress 外掛 SQL Injection 防範 | 4 招審查 AI 寫的 $wpdb

技術分享

AI 文章延伸

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,每個出現的地方逐條跑一遍:

  1. 有沒有用 prepare? 看到 $wpdb 直接拼字串、後面沒接 prepare,最基本的漏洞,先標起來。
  2. 是不是只 prepare 一半? prepare 的 SQL 範本裡,只要有變數直接內嵌、不是填空格,那個變數就沒被保護。
  3. 填空格有沒有放錯位置? 填空格出現在排序、欄位名、表格名這類「填名稱」的地方擋不住,要改用白名單。
  4. 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 卻只看功能,能抓出「用錯」比抓出「沒用」需要更深一層的眼力。

關鍵概念:SQL 注入防範縱深防禦

常見問題 FAQ

$wpdb 一定要用嗎?用 WordPress 內建函式會比較安全嗎?

只要內建函式(WP_Queryget_optionget_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 官方帳號聊聊你的專案。

Google 偏好來源

把我們設為 Google 偏好來源

喜歡我們的內容嗎?一鍵將我們設為偏好來源,未來在 Google 焦點新聞與 AI 概覽中就能優先看到我們的文章。

作品案例

看看我們打造的產品與專案。從 WordPress 外掛到 AI 客服方案,每一個作品都是實戰經驗的累積。

瀏覽作品案例

服務項目

WordPress 開發、WooCommerce 電商、LINE 整合、AI 解決方案,依據你的需求提供最適合的技術服務。

瀏覽服務項目

Contact

聯絡我們

有任何技術需求、專案諮詢或合作想法,歡迎填寫以下表單或聯繫LINE官方帳號,我們會盡快回覆。

諮詢類型 必須