網站被植入惡意腳本、登入頁被別人嵌進 iframe 騙點擊、使用者的瀏覽紀錄外洩給第三方——這些常見的前端攻擊,很多時候不是因為外掛有漏洞,而是伺服器根本沒告訴瀏覽器「哪些行為該擋下來」。WordPress 安全標頭設定就是在補這道防線:透過幾行 HTTP 回應標頭,讓瀏覽器主動幫你過濾掉跨站腳本、點擊劫持、MIME 混淆這類攻擊。
問題是,多數教學只把標頭一條條列出來、貼一段 .htaccess 就結束,沒有人告訴你 CSP 一開就把 Gutenberg 編輯器、頁面建構器、Google 字型全部弄壞該怎麼辦,也沒人說 functions.php 那個方法其實會被全站快取繞過。這篇會把 CSP、X-Frame-Options、Referrer-Policy 在內的六個核心標頭講清楚,重點放在 WordPress 環境下實際會踩到的雷,以及怎麼用 Report-Only 模式安全上線,不會設一設就讓網站開天窗。
HTTP 安全標頭是什麼,為什麼 WordPress 站要設
HTTP 安全標頭是伺服器在回應每個請求時,夾帶給瀏覽器的一組指令,告訴瀏覽器該用什麼規則處理這個網頁的內容。使用者看不到這些標頭,但瀏覽器會照著執行,等於在網站程式碼之外多加一層防護。
舉例來說,X-Frame-Options 告訴瀏覽器這個頁面能不能被別的網站嵌進 iframe,藉此擋掉點擊劫持;Content-Security-Policy 則明確列出哪些來源的腳本、圖片、樣式可以載入,就算攻擊者真的把惡意腳本注入頁面,瀏覽器也會因為來源不在白名單而拒絕執行。
這件事跟 WordPress 本身無關。安全標頭是瀏覽器層級的機制,在任何網站上運作方式都一樣。但 WordPress 是全球市占最高的內容管理系統,外掛生態龐大,攻擊面相對大,OWASP 多年來都把「安全性設定錯誤」列為網站被入侵的主要原因之一,而標頭沒設正是最常見的設定缺口。對台灣的中小型網站或電商來說,這是一個成本極低、卻能擋掉一整類前端攻擊的設定。
要提醒的是,標頭只是整體防護的一環。它擋的是瀏覽器端的攻擊,不能取代定期更新、強密碼、最小權限與伺服器層的防火牆。把標頭設好,是讓既有的安全措施更完整,不是用它取代其他工作。
WordPress 一定要設的六個安全標頭與用途
先把骨架建立起來。目前業界與 OWASP Secure Headers 專案普遍建議的基準,是以下六個標頭,涵蓋了瀏覽器端絕大多數的攻擊向量。
| 標頭 | 防的是什麼 | 常見建議值 |
|---|---|---|
| Strict-Transport-Security | 連線被降級成 HTTP、中間人竊聽 | max-age=31536000; includeSubDomains |
| Content-Security-Policy | 跨站腳本(XSS)、程式碼注入 | 視站台量身訂做 |
| X-Frame-Options | 點擊劫持(頁面被嵌 iframe) | SAMEORIGIN |
| X-Content-Type-Options | MIME 類型混淆攻擊 | nosniff |
| Referrer-Policy | 來源網址外洩、使用者隱私 | strict-origin-when-cross-origin |
| Permissions-Policy | 限制瀏覽器 API(鏡頭、麥克風、定位) | 依需求關閉用不到的 API |
Strict-Transport-Security(簡稱 HSTS)強制瀏覽器之後一律用 HTTPS 連線,就算使用者手動輸入 http:// 或點到 HTTP 連結也會自動轉成加密連線。它的作用是封死「協定降級」這條路,攻擊者沒辦法把連線拉回未加密的 HTTP 來竊聽。max-age 是瀏覽器要記住這條規則的秒數,31536000 就是一年。要特別注意,只有在 HTTPS 已經完全正常、沒有混合內容問題的站才能開 HSTS,否則一旦憑證出狀況,使用者會連不進來。
X-Content-Type-Options 設成 nosniff,作用是要求瀏覽器嚴格依照伺服器宣告的 MIME 類型處理檔案,不要自己「猜」。少了這個標頭,攻擊者有機會把一個偽裝成圖片的 HTML 檔上傳,讓瀏覽器當成 HTML 執行,演變成 XSS。這個標頭沒有額外參數,加上就對了。
Permissions-Policy 讓你控制網頁能不能使用鏡頭、麥克風、地理定位這類瀏覽器 API。把用不到的 API 直接關掉,既保護隱私也縮小攻擊面。要注意它取代的是舊的 Feature-Policy,語法不同,若沿用舊語法在新版瀏覽器裡會被靜默忽略、等於沒設。
至於 X-XSS-Protection 這個老標頭,現代瀏覽器幾乎都已棄用,它本身容易被繞過,正確的 XSS 防線是 CSP,不需要再特別去設它。
CSP 的設定邏輯與 WordPress 常見衝突
Content-Security-Policy 是這六個標頭裡最強、也最容易把網站弄壞的一個。它的邏輯是白名單:你明確告訴瀏覽器哪些來源的腳本、樣式、圖片、字型可以載入,沒列到的一律擋掉。一個最嚴格的範例是這樣:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self';
default-src 'self' 是總則,代表預設只允許從自己網域載入資源;後面的 script-src、style-src、img-src、font-src 則分別針對腳本、樣式、圖片、字型細部指定來源。問題就在這裡:上面這份政策直接套到 WordPress 上,後台跟前台幾乎一定會壞。
原因是 WordPress 的核心、Gutenberg 區塊編輯器、佈景主題與大量外掛,都重度依賴行內腳本(inline script)與行內樣式(inline style),也就是直接寫在 HTML 裡的 <script> 與 style=""。嚴格的 CSP 預設會擋掉所有行內程式碼,於是編輯器打不開、選單失效、版面跑掉。常見的衝突來源包括:
- 行內腳本與樣式:核心與多數外掛都會輸出,純
'self'會全擋 - 頁面建構器:Elementor、WPBakery 這類工具會注入大量行內樣式
- 第三方服務:Google Fonts、Google Analytics、嵌入的 YouTube、廣告或客服外掛,網域都得逐一加進白名單
- jQuery 與管理後台:wp-admin 內部的腳本若被擋,整個後台會半癱
實務上若要讓 WordPress 跑得動,常見折衷是在 script-src 與 style-src 加上 'unsafe-inline',允許行內程式碼。但這等於放掉 CSP 對 XSS 的大半防護力,只算過渡方案。更安全的做法是改用 nonce 或 hash 機制,針對每段合法的行內腳本給一個一次性的隨機標記,瀏覽器只放行帶有正確標記的程式碼——只是這需要佈景主題與外掛配合輸出 nonce,整合成本較高。
正因為 CSP 怎麼設「完全取決於你站上裝了哪些東西」,沒有一份範本可以照抄。下面會專門講怎麼用 Report-Only 模式,先觀察自己的站到底載入了哪些來源,再把政策慢慢收緊。
X-Frame-Options 與 Referrer-Policy 怎麼設才安全又不破功能
這兩個標頭比 CSP 單純,但各有一個容易設錯的地方,值得單獨拉出來講。
X-Frame-Options 控制你的頁面能不能被嵌進 iframe,用來擋點擊劫持——攻擊者把你的登入頁透明地疊在誘餌按鈕上,使用者以為點的是 A,實際點到的是你網站上的 B。它有兩個常用值:DENY 完全禁止被任何網站嵌入,SAMEORIGIN 則允許自家網域嵌入、擋掉外部網域。對一般 WordPress 站,SAMEORIGIN 是安全又不會誤傷的選擇;若你的頁面確實需要被特定外部服務嵌入(例如某些金流或預約系統的嵌入式元件),就要改用 CSP 的 frame-ancestors 指令來指定允許的網域。
這裡有個新舊交替的細節要知道:X-Frame-Options 已逐漸被 CSP 的 frame-ancestors 取代,後者更彈性、能指定多個允許網域。但因為部分舊版瀏覽器只認得 X-Frame-Options,目前最穩的做法是兩個一起設,相容性最廣。
Referrer-Policy 控制使用者從你的網站連到別處時,要透露多少來源網址資訊。當使用者點擊外連,目標網站預設看得到他從哪個頁面過來;若你的網址帶有使用者 ID、訂單編號或 session 參數,這些資訊就可能外洩給第三方。常見的選項由寬到嚴包括:
no-referrer:完全不送來源資訊,隱私最高same-origin:只在站內導覽時送來源,連到外站就不送strict-origin:跨站時只送網域、不送完整路徑,且 HTTPS 降到 HTTP 時不送strict-origin-when-cross-origin:站內送完整路徑、跨站只送網域,是隱私與分析需求之間的平衡點
對大多數 WordPress 站,strict-origin-when-cross-origin 是合理的預設值,既保護了路徑與參數不外洩,又保留足夠的來源資訊讓 Google Analytics 這類工具運作。如果你的網址裡確實會帶敏感參數,再往 strict-origin 或 no-referrer 收。
在 WordPress 加安全標頭的四種方法比較
同樣的標頭可以用不同方式送出,差別在於套用範圍、效能與維護難度。先看比較表,再說各自的適用情境。
| 方法 | 適用伺服器 | 優點 | 要注意的地方 |
|---|---|---|---|
| .htaccess | Apache | 套用到所有回應、不受快取影響 | 寫錯可能整站 500 |
| 伺服器設定檔 | Nginx | 效能最好、最穩 | 需主機 root 權限,改完要 reload |
| functions.php | 兩者皆可 | 不用碰伺服器設定 | 換佈景就失效、會被快取繞過 |
| CDN/WAF | 兩者皆可 | 不耗主機效能、永遠送出 | 各家設定介面不同 |
Apache 的 .htaccess 是最常見的做法。在網站根目錄的 .htaccess 裡,用 Header always set 加上各個標頭即可,例如:
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
</IfModule>
改之前務必先備份 .htaccess,因為這個檔案語法寫錯會讓整站回傳 500 錯誤。
Nginx 不用 .htaccess,而是在 server 區塊裡用 add_header 設定,每行結尾建議補上 always 確保各種狀態碼都會送出標頭,改完要執行 service nginx reload 才生效。
functions.php 的方法是掛在 send_headers 這個鉤子上,用 PHP 的 header() 函式輸出標頭。它的好處是不用碰伺服器設定、在 wp-admin 的佈景檔編輯器裡就能改,但有兩個關鍵限制常被忽略:第一,標頭綁在佈景主題上,換主題就失效,所以該寫進子佈景而不是直接改主佈景;第二,這段 PHP 只在 WordPress 實際執行、產生頁面時才會跑,如果你裝了全站快取外掛,被快取的頁面是由伺服器直接吐出靜態檔,根本不會經過 PHP,標頭就被繞過了。基於這兩點,純靠 functions.php 並不是穩當的長期方案。
CDN 或 WAF 層級設定則適合已經在用 Cloudflare 這類服務的站。在 CDN 設標頭有兩個好處:標頭永遠會被送出,不受 WordPress 本身狀態影響;而且請求由 CDN 處理,不會增加主機負擔。代價是各家介面不同,得照自己用的服務操作。整體而言,能在伺服器層(.htaccess 或 Nginx)或 CDN 層設定,就優先選這些,functions.php 留作沒有伺服器存取權時的備案。
CSP 安全上線流程,從 Report-Only 到正式啟用
直接把嚴格的 CSP 套上線,幾乎注定要把網站弄壞,正確做法是先觀察、再收緊。瀏覽器為此特別提供了一個「只回報、不阻擋」的姊妹標頭 Content-Security-Policy-Report-Only,可以用來安全地測試政策。整個流程拆成四步。
第一步、先用 Report-Only 模式上線一份寬鬆政策。把標頭名稱換成 Content-Security-Policy-Report-Only,這時瀏覽器會照政策檢查每個資源,但只把違規記錄下來,不會真的擋任何東西。網站照常運作,你也不會把訪客或自己鎖在外面。
第二步、收集違規報告,盤點站上真正載入了哪些來源。開著開發者工具的主控台逛過自己的網站,前台、後台、文章編輯、結帳流程都走一遍,主控台會列出所有被政策標記的來源,例如某個 Google 字型網域、某個分析腳本、某個外掛的 CDN。這一步等於是讓瀏覽器幫你列出完整的資源清單。
第三步、根據報告把合法來源加進白名單,逐步收緊政策。把盤點到的正當網域分別加進 script-src、style-src、img-src、font-src 等指令。原則是一次只放行確定需要的來源,能用 'self' 就不要放開整個網域。WordPress 站初期通常還是得在腳本與樣式保留 'unsafe-inline' 才能正常運作,這部分可以等政策其他部分都穩定後,再評估改用 nonce 機制收掉。
第四步、確認 Report-Only 模式下不再有預期外的違規,才正式啟用。把標頭名稱從 Content-Security-Policy-Report-Only 改回 Content-Security-Policy,這時政策才真正開始阻擋。上線後仍要持續觀察一段時間,因為之後新裝的外掛、換的字型、改的金流元件都可能引入新來源,需要回頭補白名單。
這套節奏的核心是:永遠不要讓使用者當你的測試對象。先讓瀏覽器在 Report-Only 模式下把問題攤開,等你看清楚自己的站到底依賴哪些外部資源,再切到正式阻擋,就不會發生「設完 CSP 結果首頁空白」這種事。
WooCommerce 結帳頁與金流嵌入對標頭的影響
如果你的 WordPress 站跑的是 WooCommerce 或有串接金流,安全標頭要特別留意結帳頁,因為這正是收款相關元件最容易跟嚴格政策衝突的地方。這裡只客觀說明標頭與這些元件的互動關係,不涉及金流串接本身的設定。
金流服務常以兩種形式嵌入結帳頁:一種是把刷卡欄位放進 iframe,一種是載入第三方的 JavaScript 來處理付款資訊。這兩種都會直接撞到前面講的標頭:
- iframe 形式的付款元件會受
X-Frame-Options與 CSP 的frame-src/frame-ancestors影響。若你站上用了金流商提供的嵌入式刷卡框,要確認政策有允許對應網域,否則付款框會載入失敗。 - 第三方付款腳本會受 CSP 的
script-src與connect-src管制。connect-src控制的是 AJAX、WebSocket 這類背景連線,金流驗證常透過這個管道,漏掉它付款會在最後一步卡住。
實務上的處理原則,是把這些金流相關網域當成「必須加進白名單的合法來源」,在前一節的 Report-Only 盤點階段就把結帳流程完整走一遍,讓主控台把所有付款相關的外部來源列出來,再逐一加進對應指令。結帳頁牽涉實際交易,比一般頁面更不能容忍出錯,所以正式啟用 CSP 前,務必在測試環境用真實的結帳流程驗證一次付款元件能正常載入與送出。
設定後怎麼驗證標頭有沒有生效
標頭設完不能憑感覺,要實際確認瀏覽器有收到。最快的方法是打開瀏覽器的開發者工具,切到「網路」分頁,重新整理頁面後點任一個請求,在回應標頭(Response Headers)區段就能看到 Content-Security-Policy、X-Frame-Options 這些標頭有沒有出現、值對不對。
更完整的檢查可以用線上工具。Scott Helme 的 SecurityHeaders.com 會掃描你的網站,依據十個安全標頭的設定給一個 0 到 100 的分數與英文字母等級,並列出還缺哪些標頭、建議怎麼補。Mozilla Observatory 則提供更偏向整體安全態勢的評估。把網址貼進去掃一次,就能對照自己的設定有沒有確實送出。
要提醒的是,評分高不代表設定就一定適合你的站。SecurityHeaders.com 給高分的前提是政策夠嚴,但前面講過,過嚴的 CSP 可能正在默默擋掉某些正常功能。所以驗證不能只看分數,要搭配實際操作:拿到分數後,回去把前台瀏覽、後台編輯、表單送出、結帳付款都走一遍,確認功能都正常,這個分數才有意義。先求站能正常運作,再求標頭分數漂亮,順序不能反過來。
把標頭從零設到能拿到不錯的等級,對 WordPress 站來說是投報率很高的一步:HSTS、X-Content-Type-Options、X-Frame-Options、Referrer-Policy 這四個幾乎可以直接照建議值設定,立刻擋掉一整類攻擊;CSP 則值得花時間,用 Report-Only 模式慢慢調到適合自己的站。今天就先用開發者工具或 SecurityHeaders.com 掃一次你的網站,看看現在送出了哪些標頭、缺了哪些,再從最沒有副作用的那幾個開始補起。