--- 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 // ❌ 危险的做法 — 存储了原始内容,但渲染时用 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 (
); } // ✅ 正确做法 — 让 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 保护**。``、`