~/blog/wordpress-sql-injection-security-guide.md
網站安全與防護 · 2025 / 09 / 15 · 4 views

SQL Injection 打不穿 WordPress 核心?真正的破口其實在這幾個地方

Eric — 浪花科技創辦人 / AI 架構師
Eric
浪花科技創辦人 · AI 架構師
SQL Injection 打不穿 WordPress 核心?真正的破口其實在這幾個地方
目錄 table-of-contents.md

一分鐘先抓重點:WordPress 怎麼擋住 SQL Injection?

SQL Injection(SQL 注入)是攻擊者把惡意的 SQL 指令「夾帶」進你的查詢語句,騙資料庫執行非預期動作,進而竊取、竄改或刪除資料。對 WordPress 來說,核心相對安全,真正的破口幾乎都在外掛、佈景主題與工程師自己寫的客製化查詢

結論先講:開發者只要記住一條鐵律——任何代入變數的資料庫查詢,一律用 $wpdb->prepare();網站管理員則做好「保持更新、最小權限、部署 WAF」三件事。把這兩面都顧好,SQL Injection 這個老問題就能被擋在門外。

SQL Injection 是個老掉牙卻極度致命的問題,一個外掛或一段客製程式碼的小疏忽,就可能讓駭客把你的資料庫當成提款機來去自如。這篇文章我會用工程師的視角,從原理、實戰寫法到管理面防禦,帶你把 WordPress 的防火牆做到滴水不漏。

SQL Injection 到底是什麼?用白話文說給你聽

先別急著看程式碼。想像你在圖書館的查詢系統找書,系統有個欄位讓你輸入「書名」,然後拿去資料庫查。

  • 正常使用者:輸入「哈利波特」,系統執行的指令是:SELECT * FROM books WHERE title = '哈利波特';
  • 搞事的駭客:輸入 ' OR '1'='1' --,系統組出來的指令變成:SELECT * FROM books WHERE title = '' OR '1'='1' -- ';

看懂了嗎?因為 '1'='1' 永遠成立(true),這個指令會繞過書名檢查,把所有書全列出來;後面的 -- 是註解符號,讓原本語句剩下的部分失效、避免報錯。這只是最簡單的例子,駭客還能用更複雜的手法竊取 users 表單、刪除資料(DROP TABLE),甚至試圖取得更高的控制權。

一句話總結:SQL Injection 就是利用程式沒有嚴格區分「指令」與「資料」的漏洞,把惡意 SQL 注入到查詢語句中,欺騙資料庫執行非預期動作。這就像你叫外送,結果外送員除了送餐,還順手複製了一把你家鑰匙,後果不堪設想。

問題的根源:把「資料」和「指令」混在一起

所有 SQL Injection 的本質都一樣:程式用字串拼接的方式,把使用者輸入直接組進 SQL 語句裡。當資料和指令攪在同一條字串,資料庫就無法分辨哪段是你的查詢結構、哪段只是要被查詢的值。攻擊者只要在輸入裡塞進引號、註解符號或邏輯運算,就能改寫查詢的語意。

理解了這個根源,後面的防禦邏輯就很清楚了:只要讓資料庫永遠知道「指令是指令、資料是資料」,注入就無從發生——這正是預備陳述式(Prepared Statements)要解決的事。

WordPress 也會中招?揭開三大資安破口

很多人有個迷思:「我用的是 WordPress 這種大平台,核心應該很安全吧?」沒錯,WordPress 核心團隊在資安上下了很大功夫,核心本身相對安全。但問題往往不在主堡,而在周邊的防禦工事,也就是整個 WordPress 生態系。

破口一:萬年不更新的外掛與佈景主題

這是最常見、也最無腦的送頭方式。許多外掛或主題被發現 SQL Injection 漏洞後,開發者會很快釋出修補更新。但如果你疏於更新,網站就等於掛著一塊「歡迎駭客光臨」的牌子。看到更新通知,別囉嗦,快點按下去。

破口二:來路不明的「免費」資源

網路上找來的「開心版」、「破解版」付費外掛或主題,幾乎都被加料過,藏有後門或惡意程式碼。貪小便宜的下場,往往是付出更慘痛的代價。天下沒有白吃的午餐,在軟體世界尤其如此。

破口三:工程師自己寫的「客製化」漏洞(這最致命)

這是我最想強調的一點。專案常需要客製化功能,我們會自己寫與資料庫互動的程式碼。如果沒有遵循最佳實踐,這就是最大的漏洞來源。就算是資深工程師,趕專案時一時手滑,也可能寫出有漏洞的查詢——這比什麼都可怕,因為它繞過了平台原本的保護。

工程師的聖杯:用 $wpdb->prepare() 打造金鐘罩

前面囉嗦這麼多,終於進入正題。如果你是開發者,請把這句話刻進腦子裡:在 WordPress 中,任何要代入變數的資料庫查詢,都必須、一定、絕對要使用 $wpdb->prepare()

這不是建議,是命令。每次在 code review 看到沒用 prepare() 的裸奔查詢,我的血壓都直接飆高。我們直接來看範例。

錯誤的示範(千萬別學)

假設我們要根據使用者傳來的 ID 查詢一篇文章:

<?php
global $wpdb;
$post_id = $_GET['id']; // 直接從使用者輸入取得 ID
$results = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}posts WHERE ID = $post_id" );
?>

這段程式碼就是典型的 SQL Injection 漏洞。如果使用者在 URL 傳入的不是數字,而是一段惡意 SQL,你的資料庫就準備被看光光了。

正確的寫法:使用 $wpdb->prepare()

$wpdb->prepare() 扮演資料庫守門員的角色。它會把你的 SQL 指令「範本」和要填入的「資料」分開處理,確保你傳入的資料就只是資料,絕對不會被當成指令執行。這就是所謂的「預備陳述式(Prepared Statements)」。

<?php
global $wpdb;
$post_id = $_GET['id']; // 從使用者輸入取得 ID

// 使用 prepare() 來處理查詢
$query = $wpdb->prepare(
    "SELECT * FROM {$wpdb->prefix}posts WHERE ID = %d",
    $post_id
);

$results = $wpdb->get_results( $query );
?>

看到了嗎?我們把查詢中的變數部分用一個「預留位置(placeholder)」%d 代替,再把真正的變數 $post_id 當成第二個參數傳入。這樣一來,就算駭客傳入惡意字串,prepare() 也會把它當成一個普通的數字或字串來處理,而不是 SQL 指令的一部分,從根本上杜絕了注入的可能。

佔位符怎麼選?%d%s%f 各代表什麼?

選錯佔位符,防護就會打折扣。請依照資料型別正確使用:

佔位符對應型別使用時機
%d整數(decimal)ID、數量、分頁等整數值
%s字串(string)標題、關鍵字、Email 等文字值
%f浮點數(float)價格、評分等帶小數的數值

實務上有個常見誤區:不要自己在 %s 外面再補引號prepare() 會自動為字串補上必要的引號與跳脫,如果你寫成 '%s',反而可能造成多餘的引號。直接寫 WHERE post_title = %s 即可。

幾個容易被忽略、卻很常出包的進階情境

基本款學會之後,這幾個情境特別容易在客製化程式碼裡踩雷,原理仍是同一招——讓每一個動態值都走 prepare()

  • 多個變數:範本裡放幾個佔位符,後面就依序傳幾個參數。例如 $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}posts WHERE post_status = %s AND post_author = %d", $status, $author_id )
  • LIKE 模糊查詢:使用者輸入裡若含有 %_ 這類萬用字元會干擾比對,應先用 $wpdb->esc_like() 跳脫,再放進 %s。例如 $like = '%' . $wpdb->esc_like( $keyword ) . '%';,查詢寫成 WHERE post_title LIKE %s 並傳入 $like
  • IN (...) 多值查詢:不能把整串值塞進一個 %s。正確做法是依陣列長度動態組出對應數量的佔位符,再把陣列展開傳入,讓每個值各自被處理。

核心觀念只有一句:凡是「會變動、來自外部」的值,就交給 prepare() 的佔位符;凡是「固定、由你寫死」的結構(欄位名、表單名),才直接寫進範本字串。把這條界線守住,注入就沒有縫隙可鑽。

表單名與欄位名怎麼辦?千萬別直接拼接使用者輸入

佔位符是給「值」用的,無法用來代入表單名或欄位名這類「識別碼」。所以表單名、欄位名絕對不能來自使用者輸入後直接拼接。安全的做法是用白名單比對:先準備一份允許的欄位清單,使用者傳來的值只能從清單裡選,命中才使用、沒命中就拒絕。表單前綴則一律使用 $wpdb->prefix,不要自己寫死。這樣即使有人想在欄位名上動手腳,也進不了你的查詢。

不只靠程式碼!網站管理員的防禦三部曲

就算你不是工程師,也別覺得事不關己。網站安全是每個人的責任。身為網站管理員,你可以做這幾件事,大幅降低風險。

第一部:更新、更新、再更新

很重要所以講三次。WordPress 核心、外掛、佈景主題,只要有更新就馬上處理。很多主機商也提供自動更新功能,請務必開啟。記得,許多漏洞的修補就藏在這些不起眼的小版本更新裡。

第二部:使用者權限,最小化原則

不要貪圖方便給所有人「管理員」權限。一個小編只需要「作者」或「編輯」權限就夠了,他不需要安裝外掛或修改網站設定。遵循「最小權限原則(Principle of Least Privilege)」,能在帳號萬一被盜時,有效限制損害範圍。

第三部:部署 WAF(網站應用程式防火牆)

WAF 就像你網站的保全,站在第一線過濾所有進來的請求。許多知名的 WAF 服務(如 Cloudflare)都能辨識常見的 SQL Injection 攻擊模式,在它們到達你的網站前就直接攔截。這是一層非常有效的外部防護。但要提醒:WAF 是輔助的外層防線,它不能取代程式碼層面的 prepare(),兩者要一起做才算完整。

結論:安全是日常,不是口號

SQL Injection 雖然是老派攻擊手法,但它之所以歷久不衰,正是因為它利用了最基本的人性弱點:疏忽與便宜行事。無論你是開發者還是管理者,都不能對資安掉以輕心。

對開發者來說,把 $wpdb->prepare() 養成肌肉記憶,是最基本的職業道德;對管理者來說,保持更新、謹慎授權、善用工具,是守護網站資產的基本功。資安不是一次性的工作,而是一種持續的習慣與態度。

如果你對網站安全性有疑慮,或遇到棘手的資安問題不知如何下手,別擔心。浪花科技團隊擁有多年 WordPress 開發與維運經驗,能為你提供專業的資安健檢與解決方案。歡迎與我們聯繫,一起打造更安全的網路世界。

延伸閱讀

// FAQ

常見問題

什麼是 SQL Injection(SQL 注入)?
SQL Injection 是攻擊者把惡意的 SQL 指令夾帶進查詢語句,欺騙資料庫執行非預期動作,進而竊取、竄改或刪除資料。它的本質是程式用字串拼接的方式把使用者輸入直接組進 SQL,導致資料庫無法區分哪段是指令、哪段是資料。攻擊者只要在輸入中塞入引號、註解符號或邏輯運算,就能改寫查詢語意。
WordPress 開發中如何防止 SQL Injection?
核心鐵律是:任何要代入變數的資料庫查詢,一律使用 $wpdb->prepare()。它會把 SQL 指令範本與要填入的資料分開處理,透過 %d、%s、%f 等預留位置代入變數,確保傳入的資料只會被當成資料、不會被當成指令執行,從根本杜絕注入。
$wpdb->prepare() 的佔位符 %d、%s、%f 分別代表什麼?
%d 對應整數,用於 ID、數量、分頁等整數值;%s 對應字串,用於標題、關鍵字、Email 等文字值;%f 對應浮點數,用於價格、評分等帶小數的數值。要注意 %s 不需自己再補引號,prepare() 會自動加上必要的引號與跳脫,若寫成 '%s' 反而會造成多餘引號。
為什麼用了 WordPress 核心還是可能中 SQL Injection?
WordPress 核心相對安全,真正的破口通常在周邊:一是萬年不更新、含已知漏洞的外掛與佈景主題;二是來路不明的「破解版」付費資源,常被植入後門;三是工程師自己撰寫的客製化查詢未遵循最佳實踐。其中客製化漏洞最致命,因為它繞過了平台原本的保護機制。
表單名與欄位名也能用 prepare() 的佔位符代入嗎?
不能。佔位符只適用於「值」,無法用來代入表單名或欄位名這類識別碼,因此表單名與欄位名絕對不能直接拼接使用者輸入。安全做法是用白名單比對,先準備一份允許的欄位清單,使用者傳來的值必須命中清單才採用;表單前綴則一律使用 $wpdb->prefix。
~/roamer-tech/newsletter // FREE
// newsletter

訂閱免費電子報

把 AI 自動化、企業系統設計與 WordPress / Laravel 開發的真實案例和可直接照做的技巧,整理成電子報寄給你。只寄精選內容、不灌垃圾信,一鍵就能退訂。

$
// final.exec()

準備好讓你的網站開始為你工作了嗎?