前端安全布防:从一次XSS攻击看输入过滤方案

一只肉包
一只肉包 正式会员正式会员认证极客认证极客
发布于 2026-07-21 00:04 ·116 浏览 ·0 回复
内容摘要

一次评论区弹窗XSS暴露出仅用正则过滤已知标签并在前端以innerHTML渲染用户输入的缺陷,团队由此将输入过滤改为白名单语义校验与充分解码,并优先在存储前做HTML转义而非删除危险字符,使用户输入以纯文本呈现。

那是一个再平常不过的周二下午,测试同事突然在群里丢了个链接,附言“评论区好像有点不对劲”。点开一看,一段 <img src=x onerror=alert(document.cookie)> 正安安静静地躺在某篇文章的留言区里。虽然只是个无害的弹窗,但在场的人都清楚——如果这段脚本被换成窃取凭据或篡改页面的恶意代码,后果不堪设想。

事故现场:漏洞是怎么被放行的

排查时发现,问题出在我们对用户输入“过度信任”上。表单提交后,文本内容经过一个简单的正则替换(只过滤了 <script> 标签)就被存储进数据库,前端又在渲染时直接把它 innerHTML 到了页面上。

这大概是很多团队都经历过的“经典翻车”:过滤规则只会匹配已知的恶意模式,比如 <script> 或 javascript:,但攻击者根本不需要走这些“老路”。<img> 标签的 onerror 事件、<svg> 的 onload 属性、甚至通过 CSS 的 expression(旧版 IE)都能让脚本改头换面。正则过滤在当时也许能拦住“小学生攻击者”,但面对一个随手翻翻 OWASP 文档的人,它就像纸糊的盾牌。

第一道防线:充分解码后的输入校验

那次事故之后,我们重新整理了思路。所谓“输入过滤”,本质上不是在过滤“标签”,而是在过滤“状态”。有效的方式是:在白名单策略下,只允许你确信的数据结构通过。

具体而言,对于用户提交的内容:

  • 严格校验语义,而不是屏蔽字符串。比如邮箱就老老实实地用 type=email 约束格式,数字就用 Number.isInteger 去验证,富文本则需要走经典的“允许标签 + 允许属性”白名单模式。只有不在名单里的标签(如 script、iframe、object)才会被剔除。
  • 优先级是“编码”而非“删除”。你不能确定未来会漏掉什么 onerror 变种,但确定的是:把它当作纯文本展示,就永远不会被解析成可执行代码。与其在服务端费力剔除危险标签,不如在存储前做一层 htmlspecialchars 转义,让 < 变成 &lt;。前端拿到这段安全的文本,用 textContent 而不是 innerHTML 渲染,就彻底断了注入的路。

纵深防御:把宝押在最后一公里

但如果你以为把“输入校验”做好就万事大吉,那还远远不够。Web 前端的攻击链往往不只存在于一个入口,XSS 的形态还有 DOM-based 和存储在第三方库里的各种变种。前端布防真正的核心,是永远假设输入是恶意的,但不再依赖输入来阻挡所有攻击。

为此我们做了两手准备:

  • 输出编码放在首位。对于任何要插入 HTML 上下文的数据,在客户端渲染时统一走 escapeHtml 工具函数,在 URL 跳转前统一做 encodeURI 并校验协议只允许 http/https。工程规范上,明确禁止在业务代码中直接拼字符串往 innerHTML 里塞东西,代码审查时就拦下。
  • 加上 CSP(内容安全策略)作为兜底。即使千防万防还有漏网之鱼,只要页面设置了 script-src 'self',浏览器就会直接拦掉所有内联脚本和外部注入,让恶意代码“哑火”。

从拦截到习惯

回看这次攻击,真正有价值的不是那几条修复代码,而是团队对“数据与代码分离”的敬畏。前端安全布防不是能一次性解决所有问题的银弹,而是一组相互补充的约定和机制。在代码里,我至今保留着那次修复时写的注释:

// 这里不用 innerHTML,也别想着只过滤 script。
// 把所有输入都当作来自最危险的黑客——转义它,然后 trust nothing。

从那以后,每当我们讨论某个功能要实现“点击评论展示高亮”时,第一个问题永远是:这些内容到底是什么,应该以什么身份出现在页面上?答案清晰了,防御自然也就有了方向。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-152.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~