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

阿乐
阿乐 管理员 黑卡会员
发布于 2026-09-07 01:06 ·3 浏览 ·0 回复

那是一个再平常不过的周二下午,测试同事突然在群里丢了个链接,附言“评论区好像有点不对劲”。点开一看,一段 `<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。

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

全部回复 0

还没有回复,来抢沙发~