全栈系列(十七) 什么是XSS/CSRF?如何防御

64 阅读7分钟

在 Web 安全领域,XSS 和 CSRF 是两大最著名的攻击方式。

第一部分:通俗解释什么是 XSS 和 CSRF

1. XSS (Cross-Site Scripting) - 跨站脚本攻击

  • 通俗解释:黑客想办法在你的网页里“埋地雷”。

  • 原理:攻击者把恶意的 JavaScript 代码注入到你的网页中。当普通用户访问你的网页时,这段代码就会在用户的浏览器里执行。

  • 后果

    • 偷走用户的 Token 或 Cookie。
    • 把用户重定向到钓鱼网站。
    • 伪造用户操作(比如偷偷发帖、转账)。
  • 例子:黑客在评论区写了一段 ,如果你的后端没过滤,前端直接渲染,每个看评论的人都会中招。

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

  • 通俗解释:黑客“借刀杀人”。
  • 原理:利用浏览器“自动发送 Cookie”的机制。假设用户登录了银行网站 A(Cookie 还在有效期),然后用户不小心点开了黑客的钓鱼网站 B。网站 B 偷偷向网站 A 发送一个“转账”请求。银行网站 A 看到请求里带着用户的 Cookie,以为是用户自己发的,于是转账成功。
  • 后果:在用户不知情的情况下,以用户的名义完成操作(改密码、发邮件、转账)。
  • 特例你的项目使用的是 JWT + Header 模式,天然免疫 99% 的 CSRF 攻击(后文详细解释)。

第二部分:后端实现方案 (Node.js + Helmet)

helmet 是 Express 生态中最著名的安全中间件,它通过设置一系列 HTTP 响应头 (Headers)  来增强安全性。

npm install helmet cors
  • helmet: 设置安全 Header。
  • cors: 控制跨域资源共享(这也是防止恶意网站请求你的 API 的第一道防线)。

2. 后端代码配置 (app.js)


import express from 'express'
import helmet from 'helmet' // 引入 helmet
import cors from 'cors'     // 引入 cors
// ... 其他引入

const app = express()

// ==========================================
// 1. 基础安全 Header 设置 (Helmet)
// ==========================================
app.use(helmet()) 
// helmet() 实际上自动启用了 10+ 个中间件,包括:
// - X-XSS-Protection: 开启浏览器的 XSS 过滤
// - X-Frame-Options: 禁止你的页面被别人的 iframe 嵌入 (防点击劫持)
// - X-Content-Type-Options: 禁止浏览器猜测文件类型
// - Strict-Transport-Security: 强制 HTTPS (HSTS)
// - ...

// ==========================================
// 2. 进阶:配置 CSP (内容安全策略) - 防御 XSS 的核武器
// ==========================================
// 如果你的图片来自阿里云OSS,或者头像来自微信,默认的 helmet 会拦截
// 所以我们需要自定义 CSP
app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"], // 默认只允许加载自己域名的资源
      scriptSrc: ["'self'"],  // 只允许运行自己服务器的 JS
      styleSrc: ["'self'", "'unsafe-inline'"], // 允许行内样式 (Vue 很多动态样式需要这个)
      imgSrc: [
        "'self'", 
        "data:", 
        "blob:", 
        "https://cube.elemecdn.com", // 允许 ElementPlus 的默认头像
        "https://thirdwx.qlogo.cn",  // 允许微信头像域名
        "https://*.aliyuncs.com"     // 允许阿里云 OSS (根据你实际情况加)
      ], 
      connectSrc: ["'self'", "https://api.weixin.qq.com"], // 允许 AJAX 请求的地址
    },
  })
)

// ==========================================
// 3. 配置 CORS - 防御 CSRF 的辅助手段
// ==========================================
// 限制只有你的前端域名/IP 才能访问接口
const whitelist = ['http://localhost:5173', 'https://你的正式域名.com']

app.use(cors({
  origin: function (origin, callback) {
    // 允许没有 origin 的请求 (比如 Postman, 手机 App, 或者同域请求)
    if (!origin) return callback(null, true)
    
    if (whitelist.indexOf(origin) !== -1) {
      callback(null, true)
    } else {
      callback(new Error('Not allowed by CORS'))
    }
  },
  credentials: true // 如果你需要发送 Cookie (虽然 JWT 不需要,但保持习惯)
}))

// ... 后续代码 (app.use 路由等)

第三部分:关于 CSRF 的特殊说明 (针对你的项目)

你目前的架构是:前后端分离 + JWT (存放在 LocalStorage/Pinia) + Header (Authorization)

结论:你的项目天生免疫 CSRF 攻击,不需要像 Session 模式那样配置 CSRF Token。

为什么?

  1. 攻击原理:CSRF 攻击依赖于浏览器自动发送 Cookie 的特性。

  2. 你的防御:你的 Token 存在前端 JS 变量或 LocalStorage 里。

    • 当黑客的网站 B 向你的后端发送请求时,浏览器不会自动把 LocalStorage 里的 Token 取出来放到 Header 里发送出去。
    • JS 是受同源策略限制的,黑客网站 B 的 JS 读不到你网站 A 的 LocalStorage。
    • 所以,黑客发出的请求没有 Authorization 头,后端直接报 401,攻击失效。

你需要做的是:确保你的 cors 配置正确,限制非法域名的访问即可。

第四部分:前端实现方案 (Vue3)

虽然 Helmet 在后端做了防护,但前端在渲染数据时也必须小心 XSS。

1. 永远不要信任用户输入

在 Vue 中,绝大多数情况下你是安全的,因为:

  • 安全:{{ message }} (Mustache 语法) 和 v-bind 会自动将 HTML 标签转义(比如把 < 变成 <),浏览器会把它当文本显示,而不是代码执行。
  • 危险:v-html。如果你必须显示富文本(比如商品详情),v-html 会直接渲染 HTML,这是 XSS 的重灾区。

2. 使用 DOMPurify 净化 (如果你用了 v-html)

如果你的商品详情、公告栏需要用到富文本编辑器,在渲染之前,必须清洗代码。

npm install dompurify

第一阶段:存储型 XSS 攻击(数据库注入)

们首先要把“恶意代码”存入数据库。假设我们在**“新增用户”“新增菜单”**时,黑客在输入框里填写的不是正常名字,而是一段代码。

1. 准备攻击载荷 (Payload)
我们将使用一段经典的利用图片加载失败触发 JS 的代码,因为它不需要 <script> 标签就能执行(很多简单的过滤器只过滤 script 标签)。

<img src=x onerror=alert('你的系统被攻击了!')>

. 执行注入

  1. 启动你的前后端项目。

  2. 进入 用户管理 页面,点击“新增用户”。

  3. 在  “昵称”  或  “备注”  栏中,直接粘贴上面的代码:<img src=x onerror=alert('你的系统被攻击了!')>。

  4. 点击保存。

    • 此时,数据库里的这个字段存的就是这段 HTML 字符串。

第二阶段:前端渲染测试(见证防御效果)

现在我们回到用户列表页,看看这段代码会被如何渲染。

实验 A:Vue 的默认防御 (Mustache 语法)

查看你的 index.vue 代码,通常我们是用 {{ }} 或者 Element Plus 的 prop 来显示数据的:

<!-- 你的代码可能是这样的 -->
<el-table-column prop="nickname" label="昵称" />

预期结果

  1. 刷新页面。
  2. 你会在表格的“昵称”列看到原封不动的字符串:<img src=x onerror=alert('你的系统被攻击了!')>。
  3. 没有弹窗出现。

Vue 的默认防御生效了。Vue 会自动把特殊字符转义(例如 < 转义为 <),浏览器只把它当文本看,不当代码执行。

实验 B:模拟漏洞 (裸奔的 v-html)

为了证明攻击是有效的,我们需要故意写一段不安全的代码。找一个页面的角落,或者临时修改 index.vue。

修改 views/system/user/index.vue,在表格上方临时加一段:

<div class="danger-zone">
  <h3>模拟黑客攻击区域:</h3>
  <!-- 假设 userList[0] 是你刚才添加的那个恶意用户 -->
  <div v-if="userList.length > 0">
    <!-- 危险!直接渲染 HTML -->
    <span v-html="userList[0].nickname"></span>
  </div>
</div>

预期结果

  1. 刷新页面。
  2. 浏览器弹出了 alert('你的系统被攻击了!')
  3. 页面上出现了一个破碎的图片图标(因为 src=x 是无效图片)。

image.png 结论:❌ 攻击成功。如果使用了 v-html 且不过滤,XSS 攻击就能穿透。

实验 C:DOMPurify 清洗 (修复漏洞)

现在我们用之前提到的 DOMPurify 来修复实验 B。

  1. 引入库:import DOMPurify from 'dompurify'
<div class="safe-zone">
  <h3>安全防御区域:</h3>
  <div v-if="userList.length > 0">
    <!-- 安全!先清洗再渲染 -->
    <span v-html="DOMPurify.sanitize(userList[0].nickname)"></span>
  </div>
</div>

预期结果

  1. 刷新页面。
  2. 没有弹窗
  3. 打开浏览器控制台 (F12) -> Elements,查看那段 DOM。你会发现 onerror=... 属性被 DOMPurify 偷偷删掉了,只剩下了 <img src="x">。

结论:✅ 前端 XSS 防御配置成功

第三阶段:后端 Helmet CSP 测试

Helmet 的核心作用之一是 CSP (内容安全策略),它能限制浏览器加载外部资源。

1. 准备攻击载荷
这次我们模拟黑客试图加载一个外部的恶意脚本(比如用来偷 Cookie 的脚本)。

<script src="https://www.baidu.com/img/flexible/logo/pc/result.png"></script>

2. 修改后端配置 (app.js)
确保你之前配置了 helmet,并且 CSP 策略中没有允许 hackers-site.com。

// 你之前的配置
app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"], // 只允许加载自己域名的 JS
      // ...
    },
  })
)

3. 执行测试

  1. 在前端找个地方(甚至可以直接在 public/index.html 里写),尝试引入这个外部脚本

  2. 打开浏览器控制台 (F12) -> Console

预期结果
你会看到一条红色的报错信息,类似:

Refused to load the script '...' because it violates the following Content Security Policy directive: "script-src 'self'".

结论:✅ 后端 Helmet 配置生效。即使用户的前端代码有了漏洞(比如误用了 v-html),浏览器也会因为后端的 CSP 策略而拒绝加载黑客的外部脚本,从而阻断了攻击。