JavaScript 安全與防護:XSS(跨站腳本攻擊)概覽
簡介
在 Web 開發的過程中,跨站腳本攻擊(Cross‑Site Scripting,簡稱 XSS) 是最常見且危害最大的資安問題之一。攻擊者透過在頁面中注入惡意 JavaScript 程式碼,讓受害者的瀏覽器在不知情的情況下執行這段程式,進而竊取 Cookie、偽造請求、或是植入勒索軟體。
對於前端開發者而言,XSS 不只是「要小心 innerHTML」的口號,而是一整套輸入驗證、輸出編碼、以及瀏覽器安全機制的綜合防禦。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步建立防護 XSS 的思維與技巧,適合剛入門的同學以及已有基礎的中階開發者閱讀。
核心概念
1. XSS 的類型
| 類型 | 觸發方式 | 常見情境 |
|---|---|---|
| 反射型(Reflected) | 攻擊者的惡意字串直接在 URL、表單或 HTTP Header 中回傳給瀏覽器 | 釣魚郵件中的惡意連結 |
| 儲存型(Stored) | 惡意程式碼被寫入資料庫、留言板、或檔案系統,之後每次讀取都會被執行 | 社群網站的貼文、評論 |
| DOM‑Based | 攻擊者利用前端腳本在瀏覽器端操作 DOM,無需向伺服器傳送惡意資料 | location.hash、document.URL 被直接寫入 innerHTML |
重點:不論是哪一種 XSS,最根本的防禦方式都是「對外輸出前必須編碼」以及「限制瀏覽器執行腳本的環境」。
2. 為什麼 innerHTML 容易成為攻擊入口
innerHTML 會把字串直接解析為 HTML,若字串內含 <script>、onerror、javascript: 等特殊語法,瀏覽器會立即執行。以下示意圖說明了攻擊流程:
flowchart TD
A[使用者輸入惡意字串] --> B[伺服器儲存或直接回傳]
B --> C[前端使用 innerHTML 插入]
C --> D[瀏覽器解析並執行惡意腳本]
D --> E[竊取 Cookie、發送偽造請求…]
3. 輸入驗證 vs 輸出編碼
| 防禦層面 | 說明 | 常見做法 |
|---|---|---|
| 輸入驗證(Input Validation) | 確保使用者送出的資料符合預期格式(如只能是數字、字母) | 正則表達式、白名單驗證 |
| 輸出編碼(Output Encoding) | 在資料呈現在 HTML、JavaScript、URL、或 CSS 前,將特殊字元轉換為安全的實體字元 | escapeHTML、encodeURIComponent、JSON.stringify |
| 內容安全政策(CSP) | 透過 HTTP Header 或 <meta> 設定瀏覽器只能執行來源可信的腳本 |
Content‑Security‑Policy: script-src 'self' |
最佳策略:三層防禦—先在伺服器端驗證,再在前端輸出前編碼,最後加上 CSP 作為最後防線。
程式碼範例
以下示範 5 個常見情境,說明如何正確防護與常見錯誤。每段程式碼均附上說明註解。
1️⃣ 直接使用 innerHTML(錯誤寫法)
// 假設從 URL ?msg= 取得使用者輸入
const params = new URLSearchParams(location.search);
const msg = params.get('msg'); // 可能是 <script>alert('XSS')</script>
document.getElementById('output').innerHTML = msg; // ❌ 直接插入,腳本會執行
問題:未對
msg進行任何編碼,惡意<script>直接執行。
2️⃣ 使用 textContent 替代 innerHTML(安全寫法)
const msg = new URLSearchParams(location.search).get('msg');
const el = document.getElementById('output');
el.textContent = msg; // ✅ 只會顯示文字,不會解析為 HTML
說明:
textContent會自動將所有特殊字元視為純文字,最簡單的防 XSS 手段。
3️⃣ 手動 HTML 編碼(自訂函式)
/**
* 將字串中的特殊 HTML 字元轉成實體編碼
* & < > " '
*/
function escapeHTML(str) {
return str.replace(/[&<>"']/g, (char) => {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return map[char];
});
}
// 使用範例
const raw = '<img src=x onerror=alert(1)>';
document.getElementById('output').innerHTML = escapeHTML(raw);
// 輸出結果為 <img src=x onerror=alert(1)>,不會執行腳本
提示:若專案規模較大,建議直接使用成熟的函式庫(如 lodash
_.escape)避免自行維護錯誤。
4️⃣ 使用第三方防 XSS 函式庫:DOMPurify
// 先在 HTML 中引入 CDN(或 npm 安裝)
// <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"></script>
const dirty = '<svg/onload=alert(document.cookie)>';
// DOMPurify 會自動移除危險屬性與標籤
const clean = DOMPurify.sanitize(dirty);
document.getElementById('output').innerHTML = clean; // 安全
優點:支援完整的 HTML 標準、可自訂白名單、效能佳,是實務上最常使用的防 XSS 工具。
5️⃣ 設定 Content‑Security‑Policy(CSP)作最後防線
在伺服器回應的 HTTP Header 中加入:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdnjs.cloudflare.com; object-src 'none';
或在 HTML <head> 中使用:
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' https://cdnjs.cloudflare.com; object-src 'none'">
說明:即使有漏洞,瀏覽器也會因 CSP 限制而不允許執行外部或內嵌的惡意腳本,大幅降低風險。
常見陷阱與最佳實踐
| 常見陷阱 | 為何危險 | 正確做法 |
|---|---|---|
| 只在伺服器端驗證 | 前端仍可能直接使用未編碼的資料渲染 UI | 前端亦要執行 輸出編碼(textContent、escapeHTML) |
使用 innerHTML 拼接字串 |
任何使用者可控的字串都有 XSS 風險 | 改用 createElement + appendChild 或 textContent |
忽略屬性層面的 XSS(如 onerror、style) |
攻擊者可在非 <script> 標籤中植入腳本 |
使用 DOMPurify 或自行過濾危險屬性 |
| 信任第三方 CDN | 若 CDN 被入侵,惡意腳本會直接載入 | 透過 SRI (Subresource Integrity) 檢查檔案雜湊;同時設定 CSP |
| 忘記對 URL、JSON、CSS 進行編碼 | XSS 不只在 HTML,還可能在 URL 參數或 JSON 中 | 使用 encodeURIComponent、JSON.stringify、CSS.escape 等對應函式 |
推薦的開發流程
- 需求階段:列出所有使用者輸入的入口(表單、URL、API)
- 設計階段:決定每個入口的 白名單驗證 規則(例如只能是數字、固定長度)
- 實作階段
- 前端:永遠使用
textContent或setAttribute,避免直接拼接innerHTML - 後端:在回傳資料前 再次編碼(防止儲存型 XSS)
- 全站:設定 CSP、SRI,並導入 DOMPurify 作為最後防線
- 前端:永遠使用
- 測試階段:使用自動化工具(如 OWASP ZAP、Burp Suite)模擬 XSS 攻擊,確認所有入口皆被防護
- 部署與監控:啟用瀏覽器報告(
report-uri)收集 CSP 違規事件,持續優化規則
實際應用場景
場景 1:留言板系統
- 問題:使用者可以自行輸入內容,若直接寫入資料庫再以
innerHTML顯示,會產生儲存型 XSS。 - 解法:
- 前端在送出前使用正則驗證(僅允許文字、換行)
- 後端儲存原始字串
- 讀取時使用
DOMPurify.sanitize或自行escapeHTML後再插入innerHTML - 同時在伺服器設定 CSP:
script-src 'self'
場景 2:單頁應用(SPA)使用路由參數
- 問題:React / Vue 等框架會根據 URL 參數渲染頁面,若直接把參數放入
dangerouslySetInnerHTML,會產生 DOM‑Based XSS。 - 解法:
- 取得參數後使用
decodeURIComponent,再以textContent顯示 - 若必須渲染 HTML(如富文字編輯器),先跑
DOMPurify.sanitize - 在
index.html加入 CSP,限制script-src、style-src
- 取得參數後使用
場景 3:電子郵件驗證連結
- 問題:驗證連結中常帶有 token,若在前端直接把
location.search內容寫入頁面,可能被利用植入腳本。 - 解法:
- 僅提取必要的 token,拋棄其他參數
- 使用
textContent顯示訊息,或根本不在前端顯示任何使用者輸入的文字 - 後端驗證 token 合法性,若失敗直接回傳錯誤訊息,不渲染任何 HTML
總結
跨站腳本攻擊 是前端開發者必須時刻警惕的資安隱憂。透過 輸入驗證、輸出編碼、以及內容安全政策(CSP) 三層防禦,我們可以在大多數情況下有效阻止惡意腳本的執行。
- 永遠避免 直接使用
innerHTML來渲染使用者提供的字串;改用textContent、createElement或 DOMPurify 之類的安全函式庫。 - 使用 CSP 讓瀏覽器自行過濾非授權腳本,為防禦提供最後一道保護。
- 持續測試:將自動化安全掃描納入 CI/CD 流程,確保新功能不會意外引入 XSS 漏洞。
只要在開發流程中把 「不信任任何外部字串」 當成基本原則,並配合上述的最佳實踐,XSS 的風險就能被有效控制,讓使用者在安全、可靠的環境中使用你的 Web 應用。祝開發順利,安全無慮!