做 Web 开发,XSS(跨站脚本攻击)是最常见也最容易被忽视的安全漏洞之一。我们平台早年就出过一次 XSS 漏洞,用户在简历里插了段脚本,被其他企业用户查看时触发,盗了 Cookie。后来花了大力气做了完整的 XSS 防御体系。今天把防御实践整理出来。
一、XSS 攻击的三种类型
先理清 XSS 的三种类型,防御方式各有不同:
1.1 反射型 XSS
恶意脚本通过 URL 参数传递,后端直接把参数输出到页面。用户点击恶意链接后触发。
后端代码:
| |
1.2 存储型 XSS
恶意脚本被存储到数据库,其他用户查看时触发。危害最大,因为不需要诱导用户点链接,只要访问页面就中招。
典型场景:用户昵称、个人简介、评论、简历内容等用户输入的字段,存储后展示给其他用户。
1.3 DOM 型 XSS
前端 JavaScript 直接操作 DOM,把不可信数据插入到页面中,不经过后端。
| |
二、防御原则:输入过滤 + 输出编码
XSS 防御的核心原则:永远不要信任用户输入,输出时必须根据上下文编码。
很多人以为只要在输入时过滤就行了,这是误区。输入过滤是第一道防线,但输出编码才是根本。因为同一个数据在不同上下文(HTML、属性、JavaScript、CSS、URL)需要的编码方式不同,输入时无法预知输出场景。
三、第一层:输入过滤
输入阶段做基础过滤,拦截明显的恶意输入。
3.1 白名单校验
对有明确格式要求的字段,用白名单校验:
| |
白名单是最安全的方式,能通过校验的一定是合法数据。
3.2 富文本过滤
对于允许 HTML 的字段(如富文本编辑器、简历内容),不能直接 strip_tags 把所有标签都去掉(用户需要格式化),要用 HTML 净化器做白名单过滤。
我们用的是 HTMLPurifier:
| |
关键点:
- 只允许安全的标签和属性
a标签的href只允许 http/https/mailto,禁止javascript:协议img标签的src只允许 http/https,禁止data:协议(防止 base64 注入脚本)- 禁止
on*事件属性(onclick、onload 等) - 禁止
<script>、<iframe>、<object>、<embed>等危险标签
HTMLPurifier 会自动移除危险标签和属性,还能修复不规范的 HTML。
3.3 长度限制
所有用户输入字段都要限制长度,防止超长输入攻击:
| |
四、第二层:输出编码
输出编码是 XSS 防御的核心。根据输出的上下文,用不同的编码方式。
4.1 HTML 上下文
输出在 HTML 标签内容中,用 htmlspecialchars:
| |
ENT_QUOTES 同时转义单引号和双引号,ENT_HTML5 用 HTML5 规范。
Yii2 的 Html::encode() 就是封装了这个:
| |
4.2 HTML 属性上下文
输出在 HTML 属性中,除了 htmlspecialchars,还要确保属性值用引号包裹:
| |
4.3 JavaScript 上下文
输出在 JavaScript 代码中,要用 JSON.stringify 编码:
| |
JSON_HEX_TAG 等选项把 <、>、'、"、& 转成 \uXXXX 形式,防止 </script> 突破脚本标签。
永远不要直接把用户输入拼到 JavaScript 字符串里:
| |
4.4 URL 上下文
输出在 URL 中(如 href、src),要验证协议并编码:
| |
特别注意 javascript: 协议:
| |
4.5 CSS 上下文
输出在 CSS 中,要严格限制允许的值,最好不要让用户直接控制 CSS:
| |
五、第三层:CSP(内容安全策略)
CSP 是浏览器层面的防护,通过 HTTP 头告诉浏览器哪些资源可以加载、哪些脚本可以执行。即使有 XSS 漏洞,CSP 也能限制攻击效果。
5.1 CSP 配置
| |
关键指令:
default-src 'self':默认只允许同源资源script-src:允许的脚本来源,'unsafe-inline'允许内联脚本(如果能不用就不用)img-src:允许的图片来源,data:允许 base64 图片connect-src:允许的 AJAX/WebSocket 连接目标frame-src 'none':禁止 iframeobject-src 'none':禁止 Flash 等插件base-uri 'self':限制 base 标签form-action 'self':限制表单提交目标
5.2 报告模式
上线前先用报告模式测试,不拦截只报告:
| |
收集一段时间的违规报告,确认没有误杀后再切换到强制模式。
5.3 Nonce 替代 unsafe-inline
'unsafe-inline' 会降低 CSP 的防护效果。更好的方式是用 Nonce:
| |
只有带正确 nonce 的内联脚本才能执行,攻击者注入的脚本没有 nonce,不会执行。
六、第四层:HttpOnly Cookie
XSS 攻击最常见的目的是盗取 Cookie。给 Cookie 加 HttpOnly 属性,JavaScript 就无法读取 Cookie,即使有 XSS 漏洞也盗不了。
| |
Yii2 配置:
| |
七、真实案例复盘
说个我们平台出过的 XSS 漏洞。
7.1 漏洞场景
用户在简历的"自我评价"字段里输入了:
| |
这个字段是富文本,我们用了 HTMLPurifier 过滤,但配置里允许了 img 标签和 onerror 属性(配置错误,应该禁止所有 on* 属性)。
企业用户查看这份简历时,图片加载失败触发 onerror,Cookie 被发到攻击者服务器。攻击者用 Cookie 登录企业账号,下载了大量简历。
7.2 修复过程
- 紧急修复:修改 HTMLPurifier 配置,禁止所有
on*属性,清掉数据库里已有的恶意内容 - 全面排查:审计所有用户输入字段,检查输出编码是否正确
- 加 CSP:上线 CSP 策略,限制脚本执行
- Cookie 加固:所有 Cookie 加 HttpOnly + Secure + SameSite
- 安全培训:团队做 XSS 防御培训,建立代码安全规范
7.3 教训
- HTMLPurifier 的配置要仔细审查,
on*属性必须全部禁止 - 不能只靠输入过滤,输出编码和 CSP 是兜底防线
- Cookie 加 HttpOnly 是基本操作,能大幅降低 XSS 危害
- 安全漏洞要定期审计,不能等出了事才修
八、安全检查清单
上线前做 XSS 安全检查:
- 所有用户输入字段都有格式校验或长度限制
- 富文本字段用 HTMLPurifier 等净化器过滤,禁止危险标签和属性
- 所有输出到 HTML 的数据都经过
htmlspecialchars编码 - 输出到 JavaScript 的数据用
json_encode+ HEX 选项编码 - URL 参数验证协议,禁止
javascript:等危险协议 - 所有 Cookie 设置 HttpOnly + Secure + SameSite
- 上线 CSP 策略,至少限制 script-src 和 object-src
- 定期用安全扫描工具(如 OWASP ZAP)做漏洞扫描
- 代码 Review 时关注用户输入的处理和输出编码
九、总结
XSS 防御是个系统工程,不能只靠某一层:
- 输入过滤:白名单校验 + 富文本净化,第一道防线
- 输出编码:根据上下文(HTML/属性/JS/CSS/URL)用不同编码方式,核心防线
- CSP:浏览器层面限制脚本执行,兜底防线
- HttpOnly Cookie:降低 XSS 危害,保护会话安全
- 安全审计:定期扫描 + Code Review,持续防护
XSS 漏洞的根源是"信任用户输入"。只要记住"所有用户输入都是不可信的,输出时必须编码",大部分 XSS 漏洞都能避免。安全不是一次性的工作,是持续的过程。框架和工具能帮我们减少漏洞,但最终还是要靠开发者的安全意识。