4、跨站脚本攻击(XSS)防御:XSS类型(反射型/存储型/DOM型)、内容安全策略(CSP)、HttpOnly Cookie

跨站脚本攻击,简称 XSS。说实话,这是 Web 安全里最常见、也最容易被忽视的问题。我见过太多团队,功能做得花里胡哨,结果一个 XSS 就把用户数据全漏了。

简单说,XSS 就是攻击者往你的页面里塞了一段恶意脚本。浏览器傻乎乎地执行了,然后 cookie、token、用户隐私全被偷走。嗯,咱们今天就把这事彻底讲清楚。

4.1 三种 XSS 类型,你都得认识

XSS 分三种:反射型、存储型、DOM 型。我一个个说。

4.1.1 反射型 XSS

这种最直接。攻击者把恶意脚本放在 URL 参数里,用户一点链接,服务器就把脚本“反射”回页面执行。

举个例子:

// 不安全的代码
app.get('/search', (req, res) => {
  let keyword = req.query.q;
  res.send('<h1>搜索结果:' + keyword + '</h1>');
});

如果用户访问 /search?q=<script>alert('xss')</script>,脚本就执行了。

注意:反射型 XSS 需要用户主动点击恶意链接。攻击者通常会配合钓鱼邮件或短链接来诱导点击。

我在项目中遇到过,有人把搜索框的输入直接拼到 HTML 里返回。我当时就问他:“你想想看,用户搜个 <script> 会怎样?”他脸都白了。

4.1.2 存储型 XSS

这个更危险。恶意脚本被存到数据库里,所有访问页面的用户都会中招。

典型的场景是评论区:

// 用户提交评论
POST /comment
{
  "content": "<script>stealCookie()</script>"
}

// 展示评论时直接输出
<div>{{ comment.content }}</div>

只要这条评论没被转义,每个看评论的人都会执行 stealCookie()。我见过一个论坛因此泄露了上万用户的登录态。

核心区别:反射型是一次性的,存储型是持久化的。存储型 XSS 的杀伤力通常大得多。

4.1.3 DOM 型 XSS

这种 XSS 完全在浏览器端发生。服务器不参与,纯前端代码把恶意内容写进了 DOM。

比如:

// 从 URL 获取参数
let name = new URLSearchParams(window.location.search).get('name');
// 直接插入 DOM
document.getElementById('greeting').innerHTML = '你好,' + name;

如果 URL 里 ?name=<img src=x onerror=alert(1)>,那页面就炸了。

我个人习惯,所有从 window.locationdocument.URLlocalStorage 获取的数据,都当成不可信输入处理。你永远不知道用户会传什么进来。

4.2 防御 XSS 的核心手段

说完了攻击,咱们聊聊怎么防。我总结了三个关键点。

4.2.1 输出编码与转义

这是最基础的。所有用户输入,在输出到 HTML 之前,必须转义。

常见的转义规则:

原始字符 转义后
< &lt;
> &gt;
" &quot;
' &#x27;
& &amp;

在 Java 里用 StringEscapeUtils.escapeHtml4(),在 Python 里用 html.escape(),在 JavaScript 里用 textContent 代替 innerHTML

小技巧:能用模板引擎的自动转义功能就别自己手写。比如 React 的 JSX 默认会转义,Vue 的 {{ }} 也会。但注意 v-html 不会,要慎用。

4.2.2 HttpOnly Cookie

这个我特别想强调。很多 XSS 攻击的目标就是偷 cookie。你只要给 cookie 加上 HttpOnly 标记,JavaScript 就读取不到它。

设置方式:

// Java
response.setHeader("Set-Cookie", "sessionId=abc123; HttpOnly; Secure");

// Node.js (Express)
res.cookie('sessionId', 'abc123', { httpOnly: true, secure: true });

// Python (Django)
response.set_cookie('sessionId', 'abc123', httponly=True, secure=True)

我曾经见过一个项目,所有 cookie 都没设 HttpOnly。攻击者一个 XSS 就把所有人的 session 全偷走了。你说冤不冤?

注意:HttpOnly 只能防 cookie 被脚本读取。如果攻击者通过 XSS 直接操作页面内容(比如伪造表单),HttpOnly 也救不了你。所以它只是防御链中的一环。

4.2.3 内容安全策略(CSP)

CSP 是个好东西。它告诉浏览器:哪些来源的脚本可以执行,哪些不行。就算攻击者注入了 <script> 标签,浏览器也会直接忽略。

一个典型的 CSP 头:

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

这个策略的意思是:

  • 所有资源默认只允许从本站加载
  • 脚本可以从本站和 trusted.cdn.com 加载
  • 禁止 <object><embed> 等插件

我建议你从 Content-Security-Policy-Report-Only 模式开始。它不会真的拦截,只会报告违规情况。等确认没问题了,再切换到正式模式。

避坑指南:我曾经把 CSP 设得太严,结果内联脚本全被拦截,页面功能直接瘫痪。后来我改用 nonce 或 hash 来允许特定的内联脚本。记住,CSP 要逐步收紧,别一上来就锁死。

4.3 综合防御策略

说实话,没有银弹。XSS 防御需要多层配合。

我总结了一个三层防御模型:

  1. 输入层:对所有用户输入做校验和过滤。但别完全依赖它,因为总有绕过方式。
  2. 输出层:所有输出到 HTML 的内容必须转义。这是底线。
  3. 策略层:用 CSP 和 HttpOnly 做兜底。就算前两层失效,攻击者也拿不到 cookie,脚本也执行不了。

你想想看,三层都做好,攻击者得多费劲才能突破?

4.4 常见误区

我见过不少团队在防 XSS 时犯的错,列出来给你提个醒:

  • 只过滤不转义:过滤能挡住大部分,但总有漏网之鱼。转义才是最后一道防线。
  • 只在后端做防御:DOM 型 XSS 纯前端发生,后端根本不知道。前端也得防。
  • 用了 CSP 就万事大吉:CSP 有兼容性问题,而且配置不当很容易被绕过。它是个好工具,但不是万能药。
  • 忽略 JSONP 和 eval:这些也是 XSS 的常见入口。能用 JSON.parse 就别用 eval
我的习惯:每次代码审查,我都会专门检查所有 innerHTMLdocument.writeeval 的使用。这三个函数,能不用就不用。

好了,XSS 防御就讲到这里。记住一句话:永远不要信任用户的输入。哪怕是你自己写的测试数据,也要当成恶意数据来处理。这是安全编码的第一条铁律。