diff --git a/hhs/DEV/XSS与CSRF攻击/XSS与CSRF攻击.md b/hhs/DEV/XSS与CSRF攻击/XSS与CSRF攻击.md new file mode 100644 index 0000000..a7053cd --- /dev/null +++ b/hhs/DEV/XSS与CSRF攻击/XSS与CSRF攻击.md @@ -0,0 +1,835 @@ +--- +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["攻击者注入
恶意脚本"] --> B["服务端存储
或反射回页面"] + B --> C["受害者浏览器
解析并执行"] + C --> D["Cookie 被盗
会话劫持 / 行为篡改"] + 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 — 最致命 + +攻击者将恶意脚本存入数据库,每个访问该页面的用户都会中招。经典案例:留言板注入 ``。 + +```go +// ❌ 危险的存储做法 — 未过滤直接入库 +func SaveComment(db *sql.DB, userID int, content string) error { + _, err := db.Exec("INSERT INTO comments (user_id, content) VALUES (?, ?)", userID, content) + return err +} + +// ✅ 正确做法 — 存储时不转义,渲染时编码;或使用富文本 sanitization +func SaveCommentSafely(db *sql.DB, userID int, content string) error { + // ⚠️ 纯文本场景:直接存原始内容,渲染时用 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 ( +
+ ); +} + +// ✅ 正确做法 — 让 React 自动处理转义 +function SearchResultsSafe({ query }: { query: string }) { + return
结果包含: {query}
; // React 自动 escape HTML entities +} +``` + +> [!note] React 的安全模型 +> React 默认对所有 JSX 表达式进行 HTML entity 转义(`<`、`>`、`&`)。只有在显式使用 `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); // # + 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` | ``, `` | `'none'`(已废弃但建议声明) | +| `base-uri` | `` 标签的 allowed origin | `'self'` | +| `form-action` | `
` 可提交的 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 + + +``` + +> [!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
; +} + +// 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. 用户访问恶意页面
伪造 GET /transfer?to=hacker&amt=10000 + Note over U: 浏览器自动带上 bank.com 的 Cookie + U->>B: 2. GET /transfer?to=hacker&amt=10000 + Cookie: sessionid=abc123... + B->>B: 3. 校验 Session ✓ → 执行转账 + B->>U: 4. 返回成功 + Note over A,U: 用户毫无感知,钱已被转走 +``` + +> [!question] 既然浏览器有同源策略(Same-Origin Policy),为什么攻击者能跨站发请求? +> 答案是:**并非所有 HTML 元素都受 SOP 保护**。``、`