本文 AI 產出,尚未審核

JavaScript 安全與防護:XSS(跨站腳本攻擊)概覽


簡介

在 Web 開發的過程中,跨站腳本攻擊(Cross‑Site Scripting,簡稱 XSS) 是最常見且危害最大的資安問題之一。攻擊者透過在頁面中注入惡意 JavaScript 程式碼,讓受害者的瀏覽器在不知情的情況下執行這段程式,進而竊取 Cookie、偽造請求、或是植入勒索軟體。
對於前端開發者而言,XSS 不只是「要小心 innerHTML」的口號,而是一整套輸入驗證、輸出編碼、以及瀏覽器安全機制的綜合防禦。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步建立防護 XSS 的思維與技巧,適合剛入門的同學以及已有基礎的中階開發者閱讀。


核心概念

1. XSS 的類型

類型 觸發方式 常見情境
反射型(Reflected) 攻擊者的惡意字串直接在 URL、表單或 HTTP Header 中回傳給瀏覽器 釣魚郵件中的惡意連結
儲存型(Stored) 惡意程式碼被寫入資料庫、留言板、或檔案系統,之後每次讀取都會被執行 社群網站的貼文、評論
DOM‑Based 攻擊者利用前端腳本在瀏覽器端操作 DOM,無需向伺服器傳送惡意資料 location.hashdocument.URL 被直接寫入 innerHTML

重點:不論是哪一種 XSS,最根本的防禦方式都是「對外輸出前必須編碼」以及「限制瀏覽器執行腳本的環境」。

2. 為什麼 innerHTML 容易成為攻擊入口

innerHTML 會把字串直接解析為 HTML,若字串內含 <script>onerrorjavascript: 等特殊語法,瀏覽器會立即執行。以下示意圖說明了攻擊流程:

flowchart TD
    A[使用者輸入惡意字串] --> B[伺服器儲存或直接回傳]
    B --> C[前端使用 innerHTML 插入]
    C --> D[瀏覽器解析並執行惡意腳本]
    D --> E[竊取 Cookie、發送偽造請求…]

3. 輸入驗證 vs 輸出編碼

防禦層面 說明 常見做法
輸入驗證(Input Validation) 確保使用者送出的資料符合預期格式(如只能是數字、字母) 正則表達式、白名單驗證
輸出編碼(Output Encoding) 在資料呈現在 HTML、JavaScript、URL、或 CSS 前,將特殊字元轉換為安全的實體字元 escapeHTMLencodeURIComponentJSON.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 = {
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#39;'
    };
    return map[char];
  });
}

// 使用範例
const raw = '<img src=x onerror=alert(1)>';
document.getElementById('output').innerHTML = escapeHTML(raw);
// 輸出結果為 &lt;img src=x onerror=alert(1)&gt;,不會執行腳本

提示:若專案規模較大,建議直接使用成熟的函式庫(如 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 前端亦要執行 輸出編碼textContentescapeHTML
使用 innerHTML 拼接字串 任何使用者可控的字串都有 XSS 風險 改用 createElement + appendChildtextContent
忽略屬性層面的 XSS(如 onerrorstyle 攻擊者可在非 <script> 標籤中植入腳本 使用 DOMPurify 或自行過濾危險屬性
信任第三方 CDN 若 CDN 被入侵,惡意腳本會直接載入 透過 SRI (Subresource Integrity) 檢查檔案雜湊;同時設定 CSP
忘記對 URL、JSON、CSS 進行編碼 XSS 不只在 HTML,還可能在 URL 參數或 JSON 中 使用 encodeURIComponentJSON.stringifyCSS.escape 等對應函式

推薦的開發流程

  1. 需求階段:列出所有使用者輸入的入口(表單、URL、API)
  2. 設計階段:決定每個入口的 白名單驗證 規則(例如只能是數字、固定長度)
  3. 實作階段
    • 前端:永遠使用 textContentsetAttribute,避免直接拼接 innerHTML
    • 後端:在回傳資料前 再次編碼(防止儲存型 XSS)
    • 全站:設定 CSPSRI,並導入 DOMPurify 作為最後防線
  4. 測試階段:使用自動化工具(如 OWASP ZAP、Burp Suite)模擬 XSS 攻擊,確認所有入口皆被防護
  5. 部署與監控:啟用瀏覽器報告(report-uri)收集 CSP 違規事件,持續優化規則

實際應用場景

場景 1:留言板系統

  • 問題:使用者可以自行輸入內容,若直接寫入資料庫再以 innerHTML 顯示,會產生儲存型 XSS。
  • 解法
    1. 前端在送出前使用正則驗證(僅允許文字、換行)
    2. 後端儲存原始字串
    3. 讀取時使用 DOMPurify.sanitize 或自行 escapeHTML 後再插入 innerHTML
    4. 同時在伺服器設定 CSP:script-src 'self'

場景 2:單頁應用(SPA)使用路由參數

  • 問題:React / Vue 等框架會根據 URL 參數渲染頁面,若直接把參數放入 dangerouslySetInnerHTML,會產生 DOM‑Based XSS。
  • 解法
    1. 取得參數後使用 decodeURIComponent,再以 textContent 顯示
    2. 若必須渲染 HTML(如富文字編輯器),先跑 DOMPurify.sanitize
    3. index.html 加入 CSP,限制 script-srcstyle-src

場景 3:電子郵件驗證連結

  • 問題:驗證連結中常帶有 token,若在前端直接把 location.search 內容寫入頁面,可能被利用植入腳本。
  • 解法
    1. 僅提取必要的 token,拋棄其他參數
    2. 使用 textContent 顯示訊息,或根本不在前端顯示任何使用者輸入的文字
    3. 後端驗證 token 合法性,若失敗直接回傳錯誤訊息,不渲染任何 HTML

總結

跨站腳本攻擊 是前端開發者必須時刻警惕的資安隱憂。透過 輸入驗證、輸出編碼、以及內容安全政策(CSP) 三層防禦,我們可以在大多數情況下有效阻止惡意腳本的執行。

  • 永遠避免 直接使用 innerHTML 來渲染使用者提供的字串;改用 textContentcreateElementDOMPurify 之類的安全函式庫。
  • 使用 CSP 讓瀏覽器自行過濾非授權腳本,為防禦提供最後一道保護。
  • 持續測試:將自動化安全掃描納入 CI/CD 流程,確保新功能不會意外引入 XSS 漏洞。

只要在開發流程中把 「不信任任何外部字串」 當成基本原則,並配合上述的最佳實踐,XSS 的風險就能被有效控制,讓使用者在安全、可靠的環境中使用你的 Web 應用。祝開發順利,安全無慮!