前端安全攻防指南:XSS / CSRF / 点击劫持与常见防护实践(含真实案例拆解)

一、为什么前端安全问题总是「被忽略」

很多团队都在聊性能优化、工程化、体验设计,但真正把 前端安全 写进项目规范、CodeReview Checklist 的其实不多。

直到有一天:

  • 运营说:用户被自动弹出的广告"劫持"到奇怪网站

  • 测试说:账号凭空被人改了密码

  • 安全部门说:日志里发现大量异常请求,追溯下去发现是前端页面被人玩坏了......

这些问题,80% 都能归到三个老朋友身上:

  • XSS(跨站脚本攻击)

  • CSRF(跨站请求伪造)

  • 点击劫持(Clickjacking)

本文会从这三个攻击方式入手,结合真实场景 & 代码示例,聊清楚:

  • 它们到底是怎么发生的?

  • 为什么「只改一行前端代码」就能把风险降一大截?

  • 在实际业务中,前端工程师能做什么?


二、基本认知:浏览器安全模型 & 同源策略快速回顾

在进入具体攻击方式前,有两个基础概念一定要清楚:

1. 浏览器信任模型

浏览器默认认为:

  • 用户主动打开的网站 是「相对可信」的;

  • 来自这个网站的 JS 有权:

    • 读写当前页面 DOM;

    • 发起网络请求;

    • 访问所在域下的 Cookie(如果没加 HttpOnly);

攻击者要做的,就是想办法 让自己的恶意脚本「伪装」成来自你网站的代码

2. 同源策略(Same-Origin Policy)

简单来说:协议 + 域名 + 端口 完全相同 → 同源

同源策略限制了:

  • 不同源页面之间的 DOM 访问;

  • 不同源接口之间的响应读取;

但它 并不阻止跨站请求本身

这就是 CSRF 能成立的原因之一:请求可以发出去,只是 JS 不能读响应而已。


三、XSS:从一条评论开始的灾难

3.1 XSS 是什么?

XSS(Cross-Site Scripting,跨站脚本攻击) 本质是:

用户可控的输入,未经安全处理就被当作 HTML/JS 输出到页面中,从而变成了攻击者的代码。

常见三种形态:

  1. 反射型 XSS:恶意脚本存在 URL 里,通过某个接口「原样返回」。

  2. 存储型 XSS:恶意脚本被存储在 DB / 缓存里,比如评论、昵称、签名。

  3. DOM 型 XSS :前端 JS 自己把用户输入拼成 HTML 插入 DOM(innerHTML 等)。

3.2 真实案例(简化版):评论系统里的一行 script

先看一个简化的评论模块(伪代码):

后端接口返回示例
复制代码
[
  {
    "id": 1,
    "author": "张三",
    "content": "这篇文章写得真不错!"
  },
  {
    "id": 2,
    "author": "攻击者",
    "content": "<script>alert('XSS from comment!')</script>"
  }
]
前端渲染代码(错误示例)
复制代码
<ul id="comment-list"></ul>

<script>
  fetch('/api/comments')
    .then(res => res.json())
    .then(list => {
      const ul = document.getElementById('comment-list');
      ul.innerHTML = list.map(item => `
        <li>
          <strong>${item.author}</strong>:${item.content}
        </li>
      `).join('');
    });
</script>

这里的关键点:

  • item.content 直接拼接进了 HTML

  • 浏览器在解析 innerHTML 字符串时,会把 <script>...</script> 当成正常脚本执行;

于是用户打开这个页面时,攻击者的脚本也被执行了。

实际项目里,攻击者不会只弹个 alert,更常见的是:

  • 读取 document.cookie,上传到攻击者服务器;

  • 注入恶意广告 / 伪造登录框;

  • 利用你页面里的登陆状态发起接口调用(配合 CSRF 做更复杂操作)。

3.3 修复方式:最根本的是「输出编码」

思路: 用户输入可以很"脏",关键在于输出到 HTML 时 不要让浏览器把它当成 HTML

1)手写简单的 HTML 转义函数
复制代码
function escapeHtml(str = '') {
  return str
    .replace(/&/g, '&amp;')   // 注意顺序:先转 &
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

然后改造渲染逻辑:

复制代码
ul.innerHTML = list.map(item => `
  <li>
    <strong>${escapeHtml(item.author)}</strong>:
    ${escapeHtml(item.content)}
  </li>
`).join('');

这样,<script> 就会被当成 普通文本 显示,而不是执行。

2)尽量使用 DOM API,而不是 innerHTML 拼字符串

更彻底一点,直接用 DOM API:

复制代码
ul.innerHTML = '';
list.forEach(item => {
  const li = document.createElement('li');
  const strong = document.createElement('strong');
  strong.innerText = item.author;      // innerText/ textContent 都会自动做 HTML 转义
  li.appendChild(strong);

  const textNode = document.createTextNode(':' + item.content);
  li.appendChild(textNode);

  ul.appendChild(li);
});

经验总结:

  • UI 组件层,尽量不用 innerHTML

  • 必须用时(比如渲染 Markdown),也要先走可信的解析库(如对 HTML 做白名单过滤);

3.4 前端框架下的「伪安全感」

以 React / Vue 为例:

  • 正常插值是安全的:

    // React 中

    {comment.content}
    // React 会自动对内容做转义
    {{ comment.content }}
  • 危险点在这里:

    • React:dangerouslySetInnerHTML

    • Vue:v-html

一旦你用这些指令/属性,框架就不会帮你转义,你要自己承担所有安全后果。

复制代码
<!-- 高危 -->
<div v-html="comment.content"></div>

如果确实需要 v-html(比如展示部分可信富文本),尽量做到:

  • 只对 内部配置 / 后台运营可控的内容 使用;

  • 或者前置一层 HTML 过滤(白名单标签、过滤 <script>onerror 等属性)。

3.5 再往前一层:CSP(Content Security Policy)

即使有 XSS 漏洞,CSP 也能 大幅减轻危害面

例如,在响应头里加上:

复制代码
Content-Security-Policy: default-src 'self'; script-src 'self'

含义是:

  • 只允许从当前域名加载资源;

  • 只允许页面使用本站脚本,禁止内联脚本和第三方脚本;

更严格一点的写法(简化示例):

复制代码
Content-Security-Policy: default-src 'self';
  script-src 'self' 'nonce-r4nd0m';
  object-src 'none';
  frame-ancestors 'none';

然后在 HTML 中这样写脚本:

复制代码
<script nonce="r4nd0m">
  // 只有带匹配 nonce 的内联脚本可以执行
</script>

重点:

  • CSP 不能代替 正确的输出编码;

  • 但它是非常重要的一道「安全兜底」。


四、CSRF:浏览器帮你「自动带上 Cookie」有多危险

4.1 CSRF 是什么?

CSRF(Cross-Site Request Forgery,跨站请求伪造) 利用了两个事实:

  1. 浏览器访问某个域名时,会自动带上该域名下的 Cookie;

  2. 服务器通常只通过 Cookie 判断「你是谁」,而不关心请求是从哪个页面发起的。

于是攻击者可以:

在自己控制的页面中,构造一个「对你网站的请求」,利用用户当前登录状态,替用户执行敏感操作。

4.2 一个典型案例:改密码 / 转账

假设有这么个改密码接口:

复制代码
POST /api/changePassword
Cookie: session=xxxx

oldPassword=123456&newPassword=654321

如果服务端只依赖 Cookie 鉴权,没有额外校验,那么攻击者可以在恶意页面里这么写:

复制代码
<form id="hacker" action="https://example.com/api/changePassword" method="POST">
  <input type="hidden" name="oldPassword" value="123456">
  <input type="hidden" name="newPassword" value="hacked123">
</form>

<script>
  document.getElementById('hacker').submit();
</script>

当用户在登录状态下访问了这个页面:

  • 浏览器自动带上 session=xxxx

  • 表单被偷偷提交;

  • 服务端看到这是「合法的带登录态的请求」,于是执行改密码操作;

4.3 CSRF 和 XSS 的关系

  • XSS:攻击者把脚本代码「放进你的网站里」;

  • CSRF:攻击者在自己的页面里「假装是你的网站在发请求」;

真实场景中,两者有时会 联动

  • 有 XSS 漏洞 → 攻击者可以在你的站点 JS 环境中直接发敏感请求;

  • 没 XSS 但 CSRF 保护不足 → 只要用户点进攻击者页面就可能中招;

现代浏览器支持 SameSite 属性来限制第三方 Cookie 使用:

复制代码
Set-Cookie: session=xxxx; Path=/; HttpOnly; Secure; SameSite=Lax

SameSite 的常见取值:

取值 行为简述
Strict 任何「跨站请求」都不带这个 Cookie
Lax 大多数跨站请求不带,顶层 GET 导航 会带(如点击链接)
None 跨站请求也会带,必须加 Secure(仅 HTTPS)

一般建议:

  • 强安全场景 → Strict 或 Lax

  • 需要嵌入第三方站点 iframe / 跨站接口复杂 → 再考虑 None; Secure,但要辅以其他手段

4.5 防御策略二:CSRF Token(最常见也最有效)

基本思路:

  1. 后端生成一个随机 Token,同时:

    • 通过 Cookie / 模板渲染传给前端;
  2. 前端在每次发请求时,把 Token 放在:

    • 请求体字段

    • 自定义请求头 (如 X-CSRF-Token)里;

  3. 服务端校验:

    • Cookie 里的 session + 请求中的 Token 是否匹配;

简单示例(前端 axios 拦截器):

复制代码
import axios from 'axios';

const instance = axios.create({
  baseURL: '/api',
  withCredentials: true, // 让浏览器自动带上 Cookie
});

// 简单示例:从 meta 标签中读取 CSRF Token
function getCsrfToken() {
  const meta = document.querySelector('meta[name="csrf-token"]');
  return meta ? meta.getAttribute('content') : '';
}

instance.interceptors.request.use((config) => {
  const token = getCsrfToken();
  if (token) {
    config.headers['X-CSRF-Token'] = token; // 约定一个自定义 header
  }
  return config;
});

export default instance;

后端(伪代码):

复制代码
app.post('/api/changePassword', (req, res) => {
  const csrfTokenFromHeader = req.headers['x-csrf-token'];
  const csrfTokenFromSession = req.session.csrfToken;

  if (!csrfTokenFromHeader || csrfTokenFromHeader !== csrfTokenFromSession) {
    return res.status(403).json({ message: 'CSRF validation failed' });
  }

  // 通过验证,执行后续逻辑
});

前端职责:

  • 把「获取 CSRF Token 并加到请求上」封装到统一的请求层(比如 axios 实例);

  • 不要在业务代码里一个个手动加,容易漏;

4.6 防御策略三:限制敏感操作方式 & 校验来源

  1. 敏感操作尽量用 POST / PUT / DELETE,而不是 GET

攻击者用 <img src="..."> 构造 GET 请求非常容易,用 POST 就麻烦多了(需要 form 或 JS)。

  1. 对 JSON 请求 + CORS 预检结合 CSRF 防御

通常建议:

  • 敏感接口使用 Content-Type: application/json

  • 通过 CORS 限制允许的域名;

  • 同时配合 CSRF Token;

  1. Referer / Origin 校验(辅助手段,不是主防线)

服务器可以检查:

  • OriginReferer 是否是可信域名;

  • 但这两个头在某些场景下可能被省略 / 修改,所以不能完全依赖。


五、点击劫持(Clickjacking):你的按钮不再是你的按钮

5.1 原理概述

点击劫持最经典的一句话解释:

把你的网站嵌在一个透明的 iframe 里,遮在攻击者页面的按钮上,用户以为点的是 A,其实点到了你页面里的 B。

简化 Demo:

复制代码
<!-- 攻击者页面 -->
<html>
<head>
  <style>
    iframe {
      position: absolute;
      top: 0;
      left: 0;
      opacity: 0.01;   /* 几乎不可见 */
      width: 800px;
      height: 600px;
      z-index: 999;
    }
    button.fake {
      position: relative;
      z-index: 1;
    }
  </style>
</head>
<body>
  <button class="fake">点我领取 100 元优惠券</button>

  <iframe src="https://example.com/account/delete"></iframe>
</body>
</html>

当用户点击"领取优惠券"按钮时,实际上是点击到了 iframe 里的「删除账号」按钮。

5.2 防御策略一:X-Frame-Options

HTTP 响应头:

复制代码
X-Frame-Options: DENY

含义:此页面 禁止被任何页面通过 <iframe><frame><object> 等方式嵌入

常见取值:

  • DENY:任何情况都不允许嵌入;

  • SAMEORIGIN:只能被同源页面嵌入;

  • ALLOW-FROM uri:现在基本已经不推荐,兼容性也不好。

在大多数系统后台 / 管理端页面上,直接设为 DENY 是比较稳妥的

5.3 防御策略二:CSP 的 frame-ancestors

在 CSP 中可以精细控制「谁能框住我」:

复制代码
Content-Security-Policy: frame-ancestors 'self' https://trusted.example.com;

含义:

  • 允许同源页面嵌入;

  • 允许 trusted.example.com 这个站点嵌入;

  • 其他都不行。

frame-ancestors 是 X-Frame-Options 的「升级版」,未来建议优先使用它(两者也可以同时设置以兼容老浏览器)。

5.4 防御策略三:前端自查(Frame Busting,辅助)

前端 JS 可以做一个简单的检查:

复制代码
if (window.top !== window.self) {
  // 说明当前页面被嵌入到了 iframe 中
  window.top.location = window.location.href;
}

不过:

  • 这只是「补充防线」,因为有些浏览器 / 运行环境会限制 top 的访问;

  • 新项目不建议仅依靠这种方式,还是要配合响应头;


六、把安全「工程化」:前端可以做的几件实事

很多安全问题,不是「某个开发不懂安全」导致的,而是:

  • 没有统一约束;

  • 没有统一封装;

  • 没有统一检查。

6.1 在 UI 组件层建立「安全基线」

例如,在内部组件库里:

  • 所有展示纯文本的组件,一律使用 textContent / 插值语法;

  • 所有可能展示富文本的场景,统一用一个 SafeHtml 组件包一下;

示例(React):

复制代码
type SafeHtmlProps = {
  html: string;
};

function sanitize(html: string): string {
  // 这里可以调用 DOMPurify 等库做白名单过滤
  // 这里先简单返回,真实项目千万不要直接 return!
  return html;
}

export function SafeHtml(props: SafeHtmlProps) {
  return (
    <div
      className="safe-html"
      dangerouslySetInnerHTML={{ __html: sanitize(props.html) }}
    />
  );
}

之后全项目禁止直接使用 dangerouslySetInnerHTML,必须走 SafeHtml

6.2 封装统一的请求层:自动带上安全信息

  • 封装 axios / fetch 实例;

  • 在拦截器里:

    • 自动加 CSRF Token;

    • 自动处理错误码;

    • 日志中打印关键信息,便于审计;

示例(继续沿用之前的 axios):

复制代码
import axios from 'axios';

const http = axios.create({
  baseURL: '/api',
  withCredentials: true,
});

http.interceptors.request.use((config) => {
  const token = getCsrfToken();
  if (token) {
    config.headers['X-CSRF-Token'] = token;
  }
  return config;
});

http.interceptors.response.use(
  (resp) => resp,
  (error) => {
    // 统一处理 401 / 403 等权限问题
    if (error.response && [401, 403].includes(error.response.status)) {
      // TODO: 触发统一的登出 / 提示逻辑
    }
    return Promise.reject(error);
  },
);

export default http;

业务代码只用 http.get/post不用关心安全细节的实现,难以"忘记"。

6.3 静态检查 & CodeReview 约束

可以在 ESLint / 自定义脚本里针对某些「危险用法」给出警告:

  • 使用 innerHTML / outerHTML → 全部警告;

  • 使用 evalnew Function → 禁止;

  • 直接在组件里使用 dangerouslySetInnerHTML → 禁止;

简单示例(ESLint 规则片段概念化):

复制代码
// .eslintrc.cjs
module.exports = {
  rules: {
    'no-eval': 'error',
    'security/detect-non-literal-fs-filename': 'warn',
    // 可以引入 eslint-plugin-security 或自行编写规则
  },
};

同时在 CodeReview Checklist 里明确一条:

  • 是否存在未转义/未过滤的用户输入直接输出到 DOM / HTML / JS 的场景?

七、一个真实修复过程:从 Bug 报告到安全基线升级(简化版)

下面这个案例来源于真实项目,我做了模糊化处理,但过程是真实的。

7.1 事件起因:日志里出现了奇怪的 URL

安全监控系统报警:短时间内出现大量请求,Referer 指向一个不认识的域名。

排查后发现:

  • 有用户在某篇文章评论里贴了一个「带脚本的标签」;

  • 这条评论被渲染到了前端页面;

  • 所有打开该文章的用户,都会不知不觉访问攻击者域名;

7.2 分析过程

  1. 重现场景:

    • 打开文章详情页;

    • 发现某条评论内容是:<img src="xxx" onerror="...">

    • 页面加载时,错误触发 onerror,执行了恶意脚本;

  2. 定位代码:

    • 评论展示组件使用了 v-html="comment.content"

    • 评论内容完全来自后端返回,后台没有任何过滤;

    • 前端也没有做转义 / 过滤;

7.3 修复方案(短期 + 中期 + 长期)

短期止血:

  • 后端立刻对现有评论做清洗:

    • 删除 / 替换所有 <script>onerroronload 等危险标签与属性;
  • 前端紧急发布修改,临时对 comment.content 做 HTML 转义,改为以纯文本展示;

中期处理:

  • 设计一套简化的富文本白名单:

    • 允许 <b>、<i>、<a>、<code>、<pre>...

    • 禁止所有事件属性(onxxx);

    • 禁止 <script>、<iframe>、<object> 等;

  • 后端统一做 HTML 过滤;

  • 前端仅在后台可控内容上使用 v-html,用户评论仍然走纯文本路径;

长期建设:

  • 在 CDN / 网关层增加 CSP 配置,限制脚本加载来源;

  • 把「避免直接使用 innerHTML/v-html」写进前端开发规范;

  • 在组件库中封装 SafeHtml,禁止随意使用 v-html

  • 给所有新页面加 X-Frame-Options / frame-ancestors 头,防止点击劫持;

7.4 教训总结

  • 大多数 XSS/CSRF 问题并不是「高深黑魔法」,而是非常「直白的疏忽」;

  • 一次事故,往往意味着 规范、工具、基础设施 的缺位;

  • 安全是「全链路」的:前端、后端、运维、安全同学都要协同。


八、最后的安全 Checklist(可直接贴到团队 Wiki)

如果你只想带走一个表,可以是这个------

XSS 相关

  • 所有用户可控内容输出到页面时是否做了 正确的输出编码

  • 项目中是否避免直接使用 innerHTML / outerHTML

  • 是否对 dangerouslySetInnerHTML / v-html 做了封装和限制?

  • 是否对富文本内容(运营后台配置)做了 白名单 HTML 过滤

  • 关键页面是否配置了合理的 CSP(script-srcobject-src)?

CSRF 相关

  • 所有敏感写操作(改密码、转账、修改个人信息等)是否采用 POST/PUT/DELETE,而不是 GET?

  • 是否在这些操作上启用了 CSRF Token 机制

  • Cookie 是否设置了合理的 SameSite 属性?(Lax / Strict)

  • 是否有统一的 前端请求封装,自动携带 CSRF Token?

  • 是否对关键接口做了 Origin/Referer 的辅助校验?

点击劫持相关

  • 后台 / 管理类页面是否设置了 X-Frame-Options: DENY 或 CSP frame-ancestors 'none'

  • 必须通过 iframe 嵌入的页面,是否明确指定了可信的 frame-ancestors 域名?

  • 是否在必要页面增加了 window.top !== window.self 的前端检测(作为辅助)?


写在最后

前端安全,听起来像是「安全团队」的职责,但真正能关掉那些最典型漏洞的,往往是我们这些写页面、写组件、写接口调用的前端。

  • 一点点编码习惯(少用 innerHTML / v-html);

  • 一点点工程封装(统一请求、统一输出);

  • 一点点约束(ESLint 规则、CR Checklist);

就可以让绝大多数「低成本攻击」无功而返。

相关推荐
AI分享猿41 分钟前
Java后端实战:SpringBoot接口遇异常请求,轻量WAF兼顾安全与性能
java·spring boot·安全
我命由我123451 小时前
微信开发者工具 - 模拟器分离窗口与关闭分离窗口
前端·javascript·学习·微信小程序·前端框架·html·js
E***q5391 小时前
Vue增强现实开发
前端·vue.js·ar
S***42801 小时前
JavaScript在Web中的Angular
前端·javascript·angular.js
黑幕困兽1 小时前
ehcarts 实现 饼图扇区间隙+透明外描边
前端·echarts
San301 小时前
深入理解 JavaScript 词法作用域链:从代码到底层实现机制
前端·javascript·ecmascript 6
Mu.3871 小时前
计算机网络模型
网络·网络协议·计算机网络·安全·http·https
七淮2 小时前
Next.js SEO 优化完整方案
前端·next.js
e***19352 小时前
爬虫学习 01 Web Scraper的使用
前端·爬虫·学习