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> 会执行,onload、onclick 也会触发。一旦这份 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 开头的事件属性。 onload、onclick、onerror、onmouseover 都算。这些能挂在任何元素上,比 <script> 隐蔽。一个看起来无害的 <rect> 可能带着 onload="fetch('...')",扫的时候别只盯 <svg> 根节点。
如果你的 SVG 来源可信(自己画的、Figma 直接导的),通常不会带这些,扫一遍图个安心就行。来源不明的(用户上传、第三方图库抓的),清完再内联,或者干脆别内联,用 <img> 引用更省事。
压缩 SVG,去冗余不改路径
讲完安全讲压缩。SVG 压缩的目标只有一个:去掉不影响渲染的冗余字节,让文件更小、首屏更快。它不改变任何路径或坐标,渲染结果完全一样。
可以放心去的冗余:
- 注释:
<!-- ... -->一律删,对渲染零影响。 - 编辑器元数据:Inkscape 会塞一大坨
sodipodi:、inkscape:命名空间,Figma 导出也会有自带 metadata。这些是给编辑器用的,浏览器不需要。 - 默认属性值:比如
<svg>上的version="1.1",现代浏览器根本不看。 - 多余空白和换行:把缩进和换行压平,单行排布。
- 未引用的
<defs>定义:定义了但从没被use过的内容,删掉省字节。
有一个东西千万别删:viewBox。viewBox 定义了 SVG 的内部坐标系,删掉之后图形会失去缩放能力,按 width/height 像素硬渲染,放到响应式布局里直接变形。我有一次压缩压狠了,连 viewBox 一起删,logo 在不同屏幕尺寸下歪成平行四边形。
还有个容易搞反的点:格式化 ≠ 压缩。格式化是加缩进、加换行、让代码好看,代价是体积变大。你想瘦身,要的是「压缩」按钮,不是「格式化」按钮。这两个动作方向相反,按错了文件反而会变大。
实操
内联 SVG 之前,或者拿到一份陌生的 SVG,与其在编辑器里肉眼扫,不如直接丢进 UPTools SVG 压缩格式化工具。它一边压缩一边实时预览,能看出渲染有没有变。同时会检测里面的 <script> 和事件属性,给个风险提示。处理全部在浏览器本地完成,文件不上传,可以放心贴敏感素材。
三句话总结
SVG 是 XML 不是普通图片,能内嵌 <script> 和 on* 事件属性,用 <img> 引入不执行脚本,直接内联进 HTML、用 object/embed 或作为独立文档打开都会执行,来源不明的 SVG 不要内联。要内联就先清掉 <script> 和所有 on* 属性,盯着这两类扫一遍基本就稳了。压缩只去注释、元数据、默认值、空白这些冗余,viewBox 一定要留,另外格式化是美化会变大、要瘦身用压缩。
相关文章
- 用 Math.random 生成密码为什么不安全?CSPRNG 与密码熵 用 Math.random 生成密码或 Token 是常见错误,它可被预测。本文讲清为什么必须用 crypto.getRandomValues,以及密码强度由字符集和长度共同决定。
- 为什么在线工具比本地软件更注重隐私?本地处理如何保护你的数据 同样是处理敏感文件,为什么说浏览器本地完成的在线工具反而比某些本地软件更注重隐私?本文讲清本地处理的原理、它保护了什么、以及它的边界。
- URL 里的中文和空格怎么传?encodeURI 和 encodeURIComponent 的区别 URL 里传中文搜索词或回跳地址,用 encodeURI 还是 encodeURIComponent 用错就会乱码或参数解析错乱。本文讲清两套 API 的区别和常见坑。