Hotdry.

Article

SVG清洗工程化实战:XML解析、危险元素剥离与CDATA防御

深入解析SVG注入攻击向量,提供XML结构清洗、事件属性剥离、CDATA隐藏脚本防御的工程化参数与监控清单。

2026-04-28security

在 Web 应用中接收用户上传的 SVG 文件是一项极具风险的操作。SVG 本质上是 XML 文档,可以包含脚本、CSS 表达式、外部资源引用等可执行内容。过去七年里,仅 Scratch 平台就修复了超过十个与 SVG 清洗相关的安全漏洞,平均每年近两个。这一数据足以说明:SVG 清洗不是一个可以「一次实现永久免疫」的工程问题,而是一个需要持续监控、迭代更新的安全管线。

攻击向量全景图

理解攻击向量的全貌是构建有效清洗管线的前提。SVG 的攻击面远超普通开发者的想象,除了显式的 <script> 标签外,还存在多种隐蔽的代码注入路径。

显式脚本注入是最直接的方式。攻击者可以在 SVG 中嵌入 <script type="text/javascript"> 标签,当浏览器解析该 SVG 时,脚本将获得执行机会。值得注意的是,CDATA 节并不能提供任何保护,因为浏览器同样会执行 CDATA 内的 JavaScript 代码。2019 年 Scratch 首次发现的 XSS 漏洞就是通过这一方式实现的,后续即使修复了小写 <script> 标签,大写 <SCRIPT> 仍然可以绕过基于正则表达式的过滤器。

事件属性注入是更为隐蔽的路径。SVG 元素支持所有 HTML 元素的全局事件属性,如 onloadonerroronmouseoveronfocus 等。即使删除了所有 <script> 标签,攻击者仍可通过以下方式执行代码:利用 <foreignObject> 嵌入 HTML 的 <img> 标签配合 onerror 事件,或者在 SVG 内部直接设置 onload 属性。2020 年发现的 CVE-2020-7750 正是这一路径的典型案例。

外部资源泄露是另一类危害极大的攻击向量。攻击者无需执行 JavaScript 即可窃取用户信息。通过 <image> 元素的 xlink:href 属性、CSS 中的 url() 函数、@import 规则,甚至较新的 image-set() 函数,都可以强制浏览器向攻击者控制的服务器发起请求。2022 年至 2026 年间,Scratch 平台多次修复此类 HTTP 泄露漏洞,每次都伴随着新的绕过技术出现,包括 CSS 转义序列、多重 url() 混合、CSS 变量伪装等。

样式污染是最新的攻击前沿。2026 年发现的「全页面重样式」漏洞利用了极长的 CSS transition 和强制浏览器重新计算样式的技巧,使得攻击者可以在用户会话期间篡改整个页面的外观。这种攻击甚至不需要任何脚本或外部资源,使得传统的清洗逻辑难以覆盖。

XML 结构化清洗流程

工程化 SVG 清洗应当采用分层防御策略,而非依赖单一工具或正则表达式。推荐的处理流程包含以下四个阶段。

第一阶段:基础 XML 解析与规范化。 使用标准 XML 解析器而非正则表达式处理 SVG 输入,这可以避免因大小写变化、空白符处理、命名空间差异导致的绕过。解析后应进行序列化再解析的「roundtrip」操作,消除可能被攻击者利用的畸形 XML 构造。建议使用 Node.js 环境下的 libxmljs 或 Python 环境下的 lxml 进行解析。

第二阶段:危险元素与属性剥离。 基于最小权限原则,构建白名单而非黑名单。以下元素应当被完全移除:scriptforeignObjectiframeobjectembeduse(当引用外部资源时)。以下属性应当被完全移除:所有以 on 开头的属性(如 onloadonerroronmouseoveronfocusonbluronanimationstart 等),hrefxlink:href 中包含非 data: 或相对路径的值,style 属性中包含 url()@importexpression() 等可执行 CSS 内容。实施参数建议将白名单配置外部化,便于安全团队快速响应新发现的攻击向量。

第三阶段:CDATA 与注释净化。 SVG 中的 CDATA 节是隐藏脚本的常用位置。虽然部分清洗工具会移除 CDATA,但更安全的做法是将其内容作为纯文本处理并重新编码。XML 注释 <!-- --> 内的内容同样需要关注,因为注释中可以包含看似无害但实际可被浏览器解析为可执行代码的内容。

第四阶段:CSS 内容深度清洗。 这是最容易出现绕过的环节。简单的字符串替换无法应对 url()@import 的各种变体。建议集成专门的 CSS 解析库(如 css-tree)进行语法树级别的处理,而非文本级别的正则匹配。需要特别关注的 CSS 功能包括:url() 函数的所有形式(包括转义序列)、@import 规则、image-set() 函数、cross-fade()-webkit-image-set(),以及未来可能支持的 src()image() 函数。实施参数建议配置 CSS 解析库为「严格模式」,任何无法解析的 CSS 规则都应当被移除而非原样保留。

防御纵深:超越清洗本身

清洗管线并非万能。即使采用了最严格的清洗策略,仍然可能存在尚未发现的绕过路径。因此,需要构建多层次的纵深防御体系。

沙箱隔离是最为有效的补充手段。将用户上传的 SVG 渲染在独立的 sandbox 属性受限的 iframe 中,可以将潜在攻击影响限制在隔离范围内。推荐配置为 sandbox="allow-same-origin",这允许通过 JavaScript 与沙箱内容进行受控交互,同时阻止脚本执行和外部资源加载。沙箱内的 Content-Security-Policy 应设置为 default-src 'none'; style-src 'unsafe-inline' data:; font-src data:; img-src data:,仅允许从 data: URL 加载资源。

内容安全策略应当作为最后一道防线。为包含用户 SVG 的页面配置严格的 CSP 头,禁用内联脚本(script-src 'self'script-src 'nonce-xxx'),限制资源加载来源(img-src 'self' data:),禁用 objectembediframe 等插件类元素。

存储与传输安全同样不可忽视。用户上传的 SVG 应当在服务端完成清洗后存储,而非在客户端清洗后传输。清洗结果应当记录日志,包括输入的哈希值、检测到的危险元素、处理决策等信息,便于事后审计和威胁溯源。

监控与响应参数

建立有效的监控体系是保障清洗管线长期有效的关键。以下是建议的监控指标与响应阈值。

输入验证阶段应监控:文件大小(建议上限 1MB)、XML 深度(建议上限 32 层)、元素数量(建议上限 500 个)、属性数量(建议上限 2000 个)。异常超标的输入应当触发告警并进入人工审核队列。

清洗结果阶段应监控:危险元素移除数量(每次清洗应当移除 0 个及以上)、处理耗时(建议上限 100ms,超时可能表示复杂度攻击)、清洗后文件大小变化(异常缩小可能表示关键内容被剥离,异常放大可能表示注入攻击)。

建议在持续集成流程中加入自动化 SVG 安全测试,覆盖常见的攻击向量样本,包括脚本注入、事件属性、foreignObject、HTTP 泄露、CSS 注入等类别。每次清洗库更新后都应运行完整的测试套件。

技术选型建议

在实际工程落地时,应当避免重复造轮子。推荐采用成熟的清洗库作为核心引擎:DOMPurify 是目前最为广泛使用的 HTML/SVG 清洗库,支持细粒度的配置选项社区活跃度高。对于需要服务端处理的场景,可以考虑 enshrined/svg-sanitize(PHP)或相应的 Python 实现。无论选择何种库,都应当锁定版本号并在安全公告发布后及时更新,因为清洗库的绕过漏洞同样会定期披露。

最后需要强调的是,SVG 清洗不是一次性的工程任务,而是需要持续投入的安全运营活动。建议将清洗逻辑的版本、配置快照、测试用例纳入版本控制,定期回顾清洗策略是否覆盖了新出现的攻击向量,并保持对 CSS 规范演进和浏览器安全机制的跟踪。

资料来源:本文技术细节主要参考 muffin.ink 博客关于 Scratch 平台 SVG 清洗漏洞的长期追踪研究,该系列文章详细记录了 2019 年至 2026 年间发现的十二个以上 SVG 相关安全漏洞的根因与修复过程。

security