6.前端安全之 XSS 与 CSRF

查看飞书原文 ↗5,230 字符
​
核心思想​
​
前端安全的核心在于“信任”问题。我们永远不能信任任何来自用户(或第三方)的输入。XSS 和 CSRF 是两种最常见、也最具代表性的 Web 攻击方式,它们都巧妙地利用了网站对用户或浏览器行为的“信任”。​
​
​
​
​
一、XSS (Cross-Site Scripting) 跨站脚本攻击​
​
1. 攻击原理​
​
攻击者将恶意的 JavaScript 脚本注入到网页中,当其他用户访问该网页时,这些恶意脚本就会在用户的浏览器中执行,从而窃取用户信息(如 Cookie)、操作用户行为、或在页面上展示恶意内容。​
​
​
本质: 恶意代码在受害者的浏览器上“无中生有”地执行了。​
​
​
​
​
​
2. 攻防演练:一个简单的评论区​
​
【攻】攻击演示​
​
假设一个网站的评论区,后端直接将用户提交的评论内容插入到页面的 HTML 中。​
​
易受攻击的代码 (前端渲染)​
​
// 从后端获取评论列表
const comments = [
  { author: '小明', text: '这是一条正常的评论。' },
  { author: 'Hacker', text: '<script>alert("XSS Attack! Your cookie is: " + document.cookie)</script>' }
];

// 前端直接将内容渲染到页面
const commentContainer = document.getElementById('comment-list');
comments.forEach(comment => {
  // 危险操作:直接使用 innerHTML 渲染未经过处理的用户输入
  commentContainer.innerHTML += `<div><strong>${comment.author}:</strong> ${comment.text}</div>`;
});
​
攻击效果​
​
当任何用户浏览这个评论区时:​
​
1.
浏览器在渲染 Hacker 的评论时,会遇到 <script> 标签。​
2.
浏览器会立即执行其中的 JavaScript 代码。​
3.
页面会弹出一个警告框,显示当前用户的 Cookie。攻击者可以将 alert 换成更复杂的代码,将 Cookie 发送到自己的服务器,从而盗用用户身份。​
​
【防】防御策略​
​
核心防御思路是:“数据”永远是“数据”,绝不能让它变成“代码”。​
​
策略一:HTML 转义 (Output Encoding) - 主要防御手段​
​
在将用户输入的内容输出到页面时,对具有特殊含义的 HTML 字符(如 <, >, &, ", ')进行转义。​
​
修复后的防御代码​
​
function escapeHTML(str) {
  if (!str) return '';
  return str.replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;');
}

// ... 获取评论列表的代码 ...

comments.forEach(comment => {
  // 安全的做法:先转义,再渲染
  const safeText = escapeHTML(comment.text);
  // 或者使用 textContent 属性,它会自动处理转义
  const div = document.createElement('div');
  const strong = document.createElement('strong');
  strong.textContent = comment.author + ': ';
  div.appendChild(strong);
  div.appendChild(document.createTextNode(safeText)); // 使用 createTextNode 也是绝对安全的
  commentContainer.appendChild(div);
});
​
防御效果​
​
经过转义后,Hacker 的恶意评论在 HTML 源码中会变成:​
​
<div>
  <strong>Hacker:</strong> &lt;script&gt;alert("XSS Attack! Your cookie is: " + document.cookie)&lt;/script&gt;
</div>
​
浏览器只会将这段代码作为纯文本**显示**在页面上,而不会**执行**它。用户看到的就是<script>alert(...)</script>这段字符串本身。​
​
策略二:内容安全策略 (Content Security Policy, CSP) - 深度防御​
​
CSP 通过设置 HTTP 头部,告知浏览器一个“白名单”,规定页面只允许加载和执行来自特定来源的脚本。即使攻击者成功注入了脚本,由于脚本来源不在白名单内,浏览器也会拒绝执行。​
​
示例 HTTP 头部​
​
Content-Security-Policy: script-src 'self' https://trusted.cdn.com;
​
这个策略告诉浏览器:只允许执行来自当前域名('self')和 https://trusted.cdn.com 的脚本。所有内联脚本(inline script)和来自其他域的脚本都将被阻止。​
​
​
​
​
二、CSRF (Cross-Site Request Forgery) 跨站请求伪造​
​
1. 攻击原理​
​
攻击者诱导一个已经登录的受害者,在不知情的情况下,从一个第三方恶意网站向被攻击网站发送一个伪造的请求(如转账、修改密码等)。由于这个请求是受害者浏览器发出的,会自动带上被攻击网站的 Cookie,因此服务器会误以为这是用户的真实意图并执行。​
​
​
本质: 攻击者冒用你的身份,发送了一个你并非本意的请求。请求是合法的,但并非出自你的意愿。​
​
​
​
​
​
2. 攻防演练:一个简单的转账操作​
​
【攻】攻击演示​
​
假设 bank.com 有一个通过 GET 请求就能完成转账的功能。​
​
易受攻击的 API (bank.com)​
​
// GET /transfer?to=Hacker&amount=1000
// 只要用户登录了 bank.com,访问这个 URL 就会立刻转账。
// 服务器仅通过 Cookie 判断用户身份。
​
恶意网站的代码 (hacker.com)​
​
攻击者在自己的网站上放置一个看不见的图片标签。​
​
<!-- hacker.com/malicious_page.html -->
<h1>劲爆!点击查看!</h1>
<!-- 用户看不到这个图片,但浏览器会尝试加载它 -->
<img src="http://bank.com/transfer?to=Hacker&amount=1000" width="1" height="1" style="display:none;">
​
攻击效果​
​
1.
用户登录了 bank.com,Cookie 已保存在浏览器中。​
2.
用户被诱导访问了 hacker.com 的恶意页面。​
3.
浏览器看到 <img> 标签,会立即尝试加载其 src 指向的 URL,也就是向 bank.com 发起一个 GET 请求。​
4.
根据同源策略,浏览器在发送这个跨站请求时,会自动带上 bank.com 域下的 Cookie。​
5.
bank.com 的服务器收到请求,验证 Cookie 发现是合法用户,于是执行转账操作。用户在毫不知情的情况下损失了 1000 元。​
​
【防】防御策略​
​
核心防御思路是:增加一个攻击者无法伪造的、与用户会话绑定的“凭证”。​
​
策略一:Anti-CSRF Token (同步令牌模式) - 主要防御手段​
​
1.
当用户访问表单页面时,服务器生成一个随机的、唯一的 token,并将其嵌入到表单的隐藏字段中。同时,服务器在 session 中也保存这个 token。​
2.
当用户提交表单时,这个 token 会随表单一起发送到服务器。​
3.
服务器比较表单提交的 token 和 session 中保存的 token。如果两者一致,则为合法请求;如果不一致,则拒绝请求。​
​
修复后的防御代码 (以 Node.js/Express 为例)​
​
1. 渲染表单时,加入 token​
​
// GET /show_transfer_form
app.get('/transfer', (req, res) => {
  // 生成 token 并存入 session
  req.session.csrfToken = generateRandomToken(); 
  res.render('transfer_form', { csrfToken: req.session.csrfToken });
});
​
2. HTML 表单中嵌入 token​
​
<form action="/transfer" method="POST">
  <input type="hidden" name="_csrf" value="<%= csrfToken %>">
  <input type="text" name="to" placeholder="收款人">
  <input type="text" name="amount" placeholder="金额">
  <button type="submit">转账</button>
</form>
​
3. 处理请求时,验证 token​
​
// POST /transfer
app.post('/transfer', (req, res) => {
  if (req.body._csrf !== req.session.csrfToken) {
    return res.status(403).send('Invalid CSRF Token');
  }
  // Token 验证通过,执行转账逻辑...
  // ...
  res.send('转账成功!');
});
​
防御效果​
​
攻击者在 hacker.com 无法得知 bank.com 在用户 session 中生成的 csrfToken 是什么,因此他们无法伪造一个带有正确 _csrf 值的请求。当伪造请求到达服务器时,会因为 token 验证失败而被拒绝。​
​
策略二:SameSite Cookie 属性 - 釜底抽薪​
​
通过在设置 Cookie 时增加 SameSite 属性,可以告诉浏览器在跨站请求中是否要携带这个 Cookie。​
​
Set-Cookie: session_id=...; HttpOnly; SameSite=Strict
​
•
SameSite=Strict: 最严格。任何跨站请求(包括从收藏夹打开、地址栏输入)都不会携带 Cookie。CSRF 攻击完全失效。​
•
SameSite=Lax: 较为宽松。允许在一些安全的顶级导航 GET 请求中携带 Cookie(如点击链接跳转),但对于 POST、<img>、<iframe> 等高风险的跨站请求,则不会发送 Cookie。这也能有效防御绝大多数 CSRF 攻击。​
​
这是目前浏览器层面最简单、最有效的 CSRF 防御手段。​
​
​
​
总结对比​
​
​
​
特征​
​
​
XSS (跨站脚本)​
​
​
CSRF (跨站请求伪造)​
​
​
攻击目标​
​
​
窃取用户在**客户端**的信息和权限​
​
​
冒用用户身份,执行服务端的操作​
​
​
信任利用​
​
​
网站信任了用户输入的内容​
​
​
网站信任了用户的浏览器(的Cookie)​
​
​
代码执行地​
​
​
受害者浏览器中​
​
​
攻击者无法注入或执行代码​
​
​
核心防御​
​
​
输入过滤和输出转义,不让数据变代码​
​
​
Token 或 SameSite Cookie,验证请求来源​
​
​
​

原文链接与附件索引