UPTools UPTools
安全 主笔:工具匠

SVG 直接内联 HTML 会触发 XSS?压缩与安全检测一次讲清

上个月有个同事从设计稿里扒了一个 SVG 图标,直接复制粘进了 React 组件库里。当天下午安全扫描就告警:XSS。他很冤:「不就是个图标吗,又不是用户输入,怎么就 XSS 了?」

问题不在图标,在 SVG 这格式本身。很多人把 SVG 当成「比 PNG 清晰的图片」,其实它是 XML,能干的事比图片多得多。这篇就讲两件事:什么情况下 SVG 会真的触发 XSS,以及怎么压缩 SVG 才不出幺蛾子。

SVG 为什么有 XSS 风险

SVG 全称 Scalable Vector Graphics,本质是一份 XML,用 <path><circle><rect> 这些标签描述图形。既然是 XML,它就能内嵌任意 XML 内容,包括 <script> 标签,也包括各种事件属性。

看一段最简单的恶意 SVG:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
  <circle cx="50" cy="50" r="40" fill="red"/>
  <script>alert(document.cookie)</script>
</svg>

或者用事件属性:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
  <image href="x" onload="alert(document.cookie)"/>
</svg>

这两段看起来都是合法的 SVG,正常渲染一个红色的圆,但只要脚本被解析器执行,就是一次 XSS。如果你这个 SVG 是从外部图库拉的、用户上传的、或者拼接过字符串生成的,里头装了什么你根本不知道。

三种引入方式,风险完全不一样

这是整篇最重要的一节。同样的恶意 SVG,用不同方式引入,结果天差地别。

第一种:用 <img src="x.svg"> 引入。安全。

浏览器把 <img> 里的 SVG 当成一张静态图片,不执行其中任何脚本,也不触发事件属性。这是 W3C 和 HTML 规范里明确写的,<img> 加载的 SVG 是「沙箱化」的,不能访问 DOM、不能跑 JS。所以图标库那种用 <img> 引一堆 SVG 的做法,没问题。

第二种:直接内联进 HTML。危险。

把 SVG 代码整个粘进 HTML 里,比如:

<div class="icon">
  <svg>...</svg>
</div>

这时 SVG 已经是主文档 DOM 的一部分,里面的 <script> 会执行,onloadonclick 也会触发。一旦这份 SVG 里有恶意代码,就是实打实的 XSS。组件库里常见的那种「把图标内联进来省一次 HTTP 请求」的做法,前提是这份 SVG 是你自己产出、可信任的。如果是外部来的,先清一遍脚本。

第三种:作为 CSS background,或作为独立文档打开。危险。

background-image: url('x.svg') 走的是资源加载,跟 <img> 类似不执行脚本,这个相对安全。但如果你直接在新标签页打开 .svg 文件(让浏览器以 image/svg+xml 渲染整页),里面的脚本会执行。用户点开邮件附件里那个 logo.svg,结果 cookie 被偷了,就是这么发生的。

整理一下风险边界:

引入方式是否执行脚本风险等级
<img src>安全
直接内联进 HTML高,需可信来源
CSS background相对安全
独立文档打开 / embed / object

记住一句话:能不能跑脚本,取决于 SVG 是被当图片用,还是被当文档用。

内联之前,清掉这两个东西

判断要内联的 SVG 有没有 XSS 风险,眼睛盯着两类东西就够了。

一类是 <script>...</script> 标签。 大部分恶意载荷都塞在这里。正则或 DOM 查询把所有 <script> 节点删掉,剩下再处理。

另一类是 on 开头的事件属性。 onloadonclickonerroronmouseover 都算。这些能挂在任何元素上,比 <script> 隐蔽。一个看起来无害的 <rect> 可能带着 onload="fetch('...')",扫的时候别只盯 <svg> 根节点。

如果你的 SVG 来源可信(自己画的、Figma 直接导的),通常不会带这些,扫一遍图个安心就行。来源不明的(用户上传、第三方图库抓的),清完再内联,或者干脆别内联,用 <img> 引用更省事。

压缩 SVG,去冗余不改路径

讲完安全讲压缩。SVG 压缩的目标只有一个:去掉不影响渲染的冗余字节,让文件更小、首屏更快。它不改变任何路径或坐标,渲染结果完全一样。

可以放心去的冗余:

  • 注释<!-- ... --> 一律删,对渲染零影响。
  • 编辑器元数据:Inkscape 会塞一大坨 sodipodi:inkscape: 命名空间,Figma 导出也会有自带 metadata。这些是给编辑器用的,浏览器不需要。
  • 默认属性值:比如 <svg> 上的 version="1.1",现代浏览器根本不看。
  • 多余空白和换行:把缩进和换行压平,单行排布。
  • 未引用的 <defs> 定义:定义了但从没被 use 过的内容,删掉省字节。

有一个东西千万别删viewBoxviewBox 定义了 SVG 的内部坐标系,删掉之后图形会失去缩放能力,按 width/height 像素硬渲染,放到响应式布局里直接变形。我有一次压缩压狠了,连 viewBox 一起删,logo 在不同屏幕尺寸下歪成平行四边形。

还有个容易搞反的点:格式化 ≠ 压缩。格式化是加缩进、加换行、让代码好看,代价是体积变大。你想瘦身,要的是「压缩」按钮,不是「格式化」按钮。这两个动作方向相反,按错了文件反而会变大。

实操

内联 SVG 之前,或者拿到一份陌生的 SVG,与其在编辑器里肉眼扫,不如直接丢进 UPTools SVG 压缩格式化工具。它一边压缩一边实时预览,能看出渲染有没有变。同时会检测里面的 <script> 和事件属性,给个风险提示。处理全部在浏览器本地完成,文件不上传,可以放心贴敏感素材。

三句话总结

SVG 是 XML 不是普通图片,能内嵌 <script>on* 事件属性,用 <img> 引入不执行脚本,直接内联进 HTML、用 object/embed 或作为独立文档打开都会执行,来源不明的 SVG 不要内联。要内联就先清掉 <script> 和所有 on* 属性,盯着这两类扫一遍基本就稳了。压缩只去注释、元数据、默认值、空白这些冗余,viewBox 一定要留,另外格式化是美化会变大、要瘦身用压缩。

相关文章