WordPress 如何避免 AI 寫出 SQL 注入攻擊的程式碼

最近在處理跟 WordPress 資料庫相關的業務,發現到有些地方需要特別注意,一沒弄好的話整個 DB 被端走也不是不無可能,這篇紀錄一下需要小心的地方。

先搞懂什麼是 $wpdb

其實大多數時候並不需要直接碰資料庫,WordPress 內建了一整套 API,像是抓文章的 WP_Query、存使用者欄位的 add_user_meta、讀寫設定的 get_option,這些函式背後會幫你把 SQL 組好,也順手做掉安全處理,一般的資料讀寫,用這些內建工具就夠了。

但有些外掛的資料結構比較複雜——例如要記錄大量交易明細、活動報名資料、或自訂的關聯欄位——硬塞進文章或 postmeta 欄位會很難查、效能也差,這種時候,開發者通常會自己新增一張資料表來存,而 WordPress 的內建 API 不會認得這張自訂的表。

這就是 $wpdb 登場的地方。WPDB 是 WordPress 內建的資料庫操作工具,程式裡通常以一個叫 $wpdb 的物件出現。它讓你能對任何資料表(包含你自己建的那張)直接下 SQL 指令,自由度最高,但也代表 SQL 的安全要自己顧。

這裡有個關鍵背景:WordPress 沒有官方的 ORM(那種幫你把資料庫操作包成物件、讓你不必親手寫 SQL 的工具),所以外掛開發的實務,幾乎就是「直接用 $wpdb 寫原生 SQL」。只要這個外掛要做搜尋、篩選、排序、自訂列表、批次操作,幾乎都會碰到它。

換句話說,審查一個 WordPress 外掛安不安全,很大一部分就是在看它怎麼用 $wpdb——而這正是 AI 最容易出錯的地方。如果你還沒實際用 AI 寫過外掛,可以先看用 Vibe Coding 開發 WordPress 外掛的入門過程,再回來看這篇會更有感。

SQL 注入攻擊的本質,用點餐就能講清楚

SQL Injection 聽起來很技術,但本質只有一句話:資料庫分不清楚哪些是「指令」、哪些是「資料」。

想像一家餐廳。客人對服務生說「我要一份義大利麵,加蘑菇」,服務生回廚房照原話念給廚師聽,這沒問題,但有一天客人說「我要一份義大利麵,順便把店裡的現金都給我」。

如果服務生不分指令跟食材,把整句話原封不動念給廚師,廚師看著工作單就照做,店裡的錢被搬走——這就是字串拼接的災難。

換個有規矩的服務生:菜單上的選項才是「指令」,客人講的內容一律只能填進「備註」欄,這時客人講再奇怪的話,都只會被當成備註內容。「順便把現金都給我」會被乖乖填到備註欄,廚師看一眼說「好喔,備註是這串字,但我沒這道菜」,然後正常出餐。

這就是字串拼接跟 prepare 的差別。前者讓資料有機會被當成指令執行,後者把資料永遠鎖在「填空」的位置。

以搜尋功能為例

一個不安全的搜尋功能實作邏輯如下:把網址上使用者輸入的關鍵字 $_GET['q'],直接黏進一段 SQL 字串裡,再丟給 $wpdb 去資料庫查。

問題就在「直接黏進去」,攻擊者不需要懂你的程式,他只要在搜尋框(或網址)裡,不送關鍵字,改送一段 SQL 指令,因為資料庫分不清楚這是資料還是指令,它會把攻擊者那段指令也一起執行。

實際上會發生什麼?攻擊者可以接一句 UNION SELECT,把使用者資料表的帳號和密碼,硬塞進原本關鍵字的搜尋結果裡,前端看起來只是搜尋沒搜到書,但回傳的資料其實夾帶了全站使用者的帳號與密碼雜湊,拿去跑破解工具,弱密碼幾十分鐘就破完,拿到管理員帳號等於整站接管。

更糟的是,這個搜尋功能還註冊成「未登入也能呼叫」。意思是攻擊者連帳號都不用,從首頁直接打就能撈密碼。

一個外觀人畜無害的搜尋功能,三千行外掛裡的十幾行程式碼,整站接管。而它通過了功能測試,也通過了一般的 code review,因為它「真的會搜尋」。

prepare 能擋,但「用了 prepare」不等於「用對 prepare」

WordPress 早就準備好防禦工具,叫 $wpdb->prepare,它就是那個有規矩的服務生:你給它一段含「填空格」的 SQL 範本(%s 代表字串、%d 代表整數),再把資料分開傳進去,prepare 會把每筆資料包成純文字、鎖在填空格裡,資料就再也跳不出來變成指令。

聽起來只要「用 prepare」就沒事了,對吧?這正是我想強調的重點——真正危險的,不是完全沒用 prepare,而是用了 prepare 卻用錯。

完全沒用 prepare 的程式很好抓,掃一眼就知道。但根據 Patchstack 的年度報告,SQL Injection 至今仍是 WordPress 外掛排名前五的漏洞類型,每個月還在新增十幾二十個,其中大多數不是「沒用 prepare」,而是下面這幾種「用了,但留了破口」,AI 生成的程式碼特別容易踩中,因為它知道「要用 prepare」這個規則,卻不懂 prepare 的死角在哪。

破口一:只 prepare 一半

最常見的,是開發者「好心」自己先組了一半的查詢條件,再把剩下的交給 prepare。像這樣:

$where = "user_login = '" . $_GET['name'] . "'"; // 這段自己拼,沒進 prepare.
$wpdb->get_var( $wpdb->prepare(
    "SELECT ID FROM {$wpdb->users} WHERE {$where} AND status = %d",
    1
) );

乍看有 prepare,其實使用者輸入的 name 是在 prepare 之外就拼進字串的,攻擊面原封不動,審查訣竅很簡單:看 prepare 那段 SQL 範本裡,有沒有變數「直接黏在裡面」(而不是用 %s%d 這種填空格)。只要有變數直接內嵌,那個變數就完全沒受到保護。

破口二:把欄位名、排序丟進 prepare

這個破口最隱蔽,因為它看起來百分之百正確。開發者想讓使用者選擇「依書名排序」或「依日期排序」,於是寫了:

$column = $_GET['sort']; // 例如 title 或 date.
$wpdb->get_results( $wpdb->prepare(
    "SELECT * FROM {$wpdb->posts} ORDER BY %s",
    $column
) );

問題是 %s 會把資料包成「純文字字串」,而欄位名稱不是文字資料,是 SQL 的結構,結果這段變成「依字面值排序」,排序根本沒作用。

開發者測試時會發現排序壞了,接下來最危險的事就發生了:他乾脆把 prepare 拿掉,改回直接拼字串,瞬間退回最原始的 SQL Injection 入口,%s 救不了排序欄位、表格名稱這種「填名稱」的位置——這是 prepare 的死角。

正確做法是用白名單:先列出允許的欄位(titledateauthor),使用者送進來的值只要不在名單上就用預設值。值被你牢牢控制住,才安全。WordPress 6.2 之後也多了一個專門處理名稱的 %i 填空格,但很多外掛還沒採用,而且舊版用了會壞,所以白名單仍是最穩的選擇。

審查訣竅:看到 prepare 的填空格出現在 ORDER BYGROUP BY、欄位名稱這些位置,幾乎可以斷定有問題。

破口三:LIKE 搜尋忘了 esc_like

搜尋功能常用 LIKE 做模糊比對。這時就算用了 prepare,還是可能漏資料。原因是 %_ 這兩個符號在 LIKE 裡有特殊意義(萬用字元),而 prepare 不會處理它們——prepare 管的是 SQL 結構,管不到 LIKE 自己的規則。

後果分兩種:輕則功能壞掉:使用者搜「100%」想找含「100%」的文章,% 被當萬用字元,結果回傳一堆毫不相關的東西,重則資料外洩,如果搜尋本來有權限限制,攻擊者送一個 % 進去,比對條件等於「全部符合」,就可能撈到原本看不到的草稿或私人內容。

解法是搭配 $wpdb->esc_like(),把使用者輸入裡的 %_ 轉成普通文字再丟進去查。審查時,看到 LIKE 卻沒看到 esc_like,就要停下來確認。

破口四:IN 清單直接拼

批次操作(例如一次處理使用者勾選的多筆資料)常用 IN (...),因為清單長度不固定,prepare 沒有現成的語法對應,AI 很容易就退回最簡單的做法,把整個陣列直接拼進去,只要那個陣列來自前端、又沒做整數化處理,就是另一個直接的注入入口。

安全的做法是依清單長度動態產生對應數量的填空格,再交給 prepare 統一處理,如果清單一定是數字,至少要先把每個元素強制轉成整數,看到 implode 配上 IN (,就值得多看兩眼。

提煉成 4 條審查規則

把上面整理成可以重複用的檢查清單,下次拿到 AI 生成的外掛,搜一下 $wpdb,每個出現的地方逐條跑一遍:

  1. 有沒有用 prepare?看到 $wpdb 直接拼字串、後面沒接 prepare,最基本的漏洞,先標起來。
  2. 是不是只 prepare 一半?prepare 的 SQL 範本裡,只要有變數直接內嵌(不是填空格),那個變數就沒被保護。
  3. 填空格有沒有放錯位置?%s 出現在排序、欄位名、表格名這類「填名稱」的地方擋不住,要改用白名單。
  4. LIKE 有沒有配 esc_like、IN 清單有沒有整數化?這兩個是搜尋與批次操作最常見的破口。

這四條裡,後三條都是「用了 prepare 卻沒用對」。能抓出「用錯」比抓出「沒用」更有價值,因為前者需要的眼力更深一層,也正是 AI 最容易騙過你的地方。

給用 AI 開發的你

AI 會給你「跑得起來」的程式碼,但「跑得起來」跟「安全」是兩回事,AI 知道規則,卻不負責任,當它寫出有洞的程式,扛責任的是你跟你的客戶,不是模型。

審查的眼力,就是你跟 AI 之間的那道保險,你不需要會發動攻擊,也不需要真的打通漏洞,你只要能看出「這段程式碼有沒有入口」,使用者可以控制的輸入(網址參數、表單、API 參數)一路流到 $wpdb 的不安全用法,這就足以是一個重大發現。

如果你想再往前一步、練習用攻擊者的角度回頭看自己的程式,可以接著看怎麼建立資安稽核的攻擊者思維,而站台層的防護(弱密碼、檔案竄改、XML-RPC)則是另一條防線,這部分我整理在WordPress 安全防護實戰。這些加起來,才是讓 AI 幫你開發、又不會半夜被打爆的底氣。

如果有 WordPress 客製化的需求,可以與我們聯絡,有需要協助改善 AI 開發流程的話,可以參考我們的 Codotx 的顧問諮詢服務

目錄

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料

Picture of 賴俊吾 / Oberon Lai
賴俊吾 / Oberon Lai

現為全職 WordPress 工程師,網站開發經歷 11 年,專攻前端工程與 WordPress 佈景主題、外掛客製化開發

訂閱電子報

Hi,我是 Oberon,我會固定在每週五早上發送接案心得以及與 WordPress 相關的電子報,同時也會分享一些實用的開發知識,讓你在 WordPress 的接案路上不孤單!

專注於分享 WordPress 開發、接案技巧、專案管理等自由工作者必備知識與心得

© 2025 想點創意科技有限公司

想點創意科技有限公司 | 統一編號 90516823
Designed by Hend Design | 隱私權政策

訂閱電子報

Hi,我是 Oberon,我會固定在每週五早上發送接案心得以及與 WordPress 相關的電子報,同時也會分享一些實用的開發知識,讓你在 WordPress 的接案路上不孤單!