XSS 过滤器绕过是怎么发生的,靠标签黑名单为什么不够
安全公告里的「XSS 过滤器绕过」具体是怎么发生的?一句话概括:过滤器和浏览器对同一串文本的理解不一致。过滤器按自己的规则判断这段内容是否安全,浏览器按另一套解析规则决定要不要执行。绕过的做法就是构造出在这两套规则里答案不同的输入——在过滤器眼里是普通文本,在浏览器眼里成了可执行代码。Joomla 近期的两条 InputFilter 公告正对应这两类差异:一条涉及 HTML data URI 中的空白字符处理,一条涉及 HTML5 实体解码。
理解了这一点,就能回答第二个问题:为什么只做标签黑名单过滤仍然会被绕过——黑名单检查的是「这段文本长什么样」,浏览器执行的是「这段文本被解析成了什么」,两者之间永远存在解释空间。
绕过发生在哪一层
内容进入系统一般经过两道处理。入库时做过滤:把不允许的标签、属性、协议去掉或者改写;渲染时做转义:把内容按目标位置的语法规则重新编码。
黑名单过滤的问题出在它必须自己模拟浏览器的解析规则才能判断准确,而解析规则本身在不同上下文里不同。同一段文本放在元素内容里、属性值里、JavaScript 字符串里、URL 参数里,安全写法完全不一样。过滤器只有一份规则,渲染上下文却有多种,这个不对称就是绕过发生的位置。
data URI 与实体解码这两类差异都是典型例子:空白字符会不会被当成有效分隔、实体记法被解码成什么字符,过滤器和浏览器的答案不同,检查就形同虚设。
黑名单过滤的三个失效场景
第一是编码与记法变体。同一字符可以有多种写法,过滤器认得常见几种,浏览器却全部接受。第二是上下文切换。内容在属性、脚本、URL 之间被复用时,原本安全的写法在新的上下文里变成可执行。第三是解析差异。规范里允许浏览器做兼容处理的部分,正是过滤器最难覆盖的部分。
这三个场景有一个共同点:都不是靠「加几条黑名单规则」能补上的。规则越加越长,判断成本上升,漏判概率却不会明显下降,因为问题出在规则与解析器的差异上,不在规则数量上。
入库过滤与渲染转义的分工
职责要分清:入库过滤是内容治理手段,目的是让存下来的东西大体干净,便于展示和复用;渲染转义才是防注入的执行点,因为它按输出位置的语法要求重新编码内容,让数据不再被解释成代码。
正确的做法是把两者当两道不同的工序,而不是让前者替代后者。任何来自用户输入的内容——评论、留言、自定义字段、导入的正文——在渲染时都必须按所处上下文转义,无论它在入库时是否已经过滤过。
另一条边界同样重要:跨站脚本与 SQL 注入的防护手段不是一回事。SQL 注入靠参数化查询来挡,而不是关键词表;把两者混成一类「敏感内容检查」,通常是两条防线都没立起来。
内容安全设置该配的几件事
AnQiCMS 的内置防护里,敏感词过滤与关键词替换解决的是命中规则的内容如何处置,内容审核解决的是这条记录能不能对外露出,这两项属于内容治理,不能当成注入防护来理解。防采集干扰码面向的是批量复制,与执行安全无关。
真正需要逐项确认的是这些:表单与留言的写入口是否有校验;用户提交内容在后台列表里是否同样经过转义——待审核内容虽然不在前台展示,但后台审核时仍会被浏览器渲染,这一处最容易漏;富文本编辑器的产出在渲染时是否按上下文处理;静态页面的输出编码是否统一。
三层职责对照
| 层次 | 手段 | 目的 | 挡不住什么 |
|---|---|---|---|
| 入库过滤 | 标签与属性黑名单、敏感词过滤、关键词替换 | 让存储内容大体干净 | 解析差异型绕过 |
| 渲染转义 | 按输出上下文重新编码 | 让数据不被执行 | 逻辑本身的缺陷 |
| 参数化 | 查询与参数分离 | 阻断 SQL 注入 | 与脚本执行无关 |
常见问题
过滤器有没有必要保留?有。它能减少脏内容进入存储,降低展示层的处理压力,但要把定位说清楚:内容治理,不是安全边界。
怎么验证自己有没有这类风险?提交一批包含非常规记法、编码变体与不同上下文用法的测试内容,看前台与后台渲染时是否被原样执行。检查范围要包含待审核列表,攻击者可以故意提交、等待被查看。
升级补丁能替代转义吗?不能。补丁修的是特定解析差异,转义覆盖的是所有输出位置。
一个判断口径
看一个系统怎么对待用户提交的内容,就能判断它的注入防护水位:把安全寄托在「进来的东西已经过滤过」,还是寄托在「无论进来什么,输出时都按上下文重新编码」。前者是概率,后者是机制。