写点什么

FinClip 小程序里如何安全使用 SVG

作者:Speedoooo
  • 2022 年 5 月 17 日
  • 本文字数:5782 字

    阅读完需:约 19 分钟

FinClip小程序里如何安全使用SVG

在小程序中使用 SVG,和在普通网页中不太一样。SVG 也并不仅是另一种图片格式这么简单。它是代码,需要有额外的安全考量。在小程序里成功使用 SVG 的诀窍在于(1)结合 CSS background image,(2)采用 inline 方式,(3)用对 Data URI scheme,(4)把内容转换为 base64。经本文整理,向读者提供最完整的介绍。


网上零零散散有一些关于在小程序中如何使用 SVG 的内容,但不是语焉不详,就是信息不完整。在此整理一下,供哪怕是此前从来没有接触过 SVG 的开发者也可以参考,迅速利用。首先 SVG 可以被视为一种 DSL(Domain Specific Language),也就是说它并不仅仅是 JPEG、PNG、GIF 之外的又一种图片格式,它还是一种“代码”;也因此,它既有无比的潜力让开发者发挥创意作出有意思的应用,它也有潜在安全风险。在小程序中,起码至目前为止,它的使用方式和在普通网页并不完全一样。

什么是 SVG

SVG 是 Scalable Vector Graphics 的缩写。它:

  • 用于定义矢量图

  • 是一种 XML 文本

  • 所定义的每一个元素(Element)及其属性(Attribute)均可以支持动画

  • 是 W3C 推荐的开放标准

  • 能与其他 W3C 标准如 DOM、XSL 等结合使用

  • 有以下的好处:文件能用文本编辑器编辑,虽然文件后缀是 svg,但和 JPG、PNG、GIF 等不是一回事,而是一种 XML 格式的文本 SVG 图形内容,能被索引、搜索、脚本化操作处理、压缩。例如 Google 就明确声明,它的网络爬虫会索引 SVG 图形的文本内容,因此用户可以通过 SEO 加以利用矢量图放大缩小不失真


以下的 svg 描述了一个多边形:

<svg height="250" width="500">  <polygon points="220,10 300,210 170,250 123,234" style="fill:lime;stroke:purple;stroke-width:1" /></svg>
复制代码


SVG 图形是如何被引用至网页中的

第一种,也是最简单直观的方式,即把 svg 后缀的文件视作为和 PNG、JPEG、GIF 类似的图片:

<img src="image.svg" />
复制代码


第二种,当嵌入的 svg 文件需要引用外部资源(例如字体、脚本、其他 bitmap 类型的二进制图片或者其他 svg),或者对内容有一定的交互和处理,<img>标签无法支持,这时可以采用<object>标签:

<object type="image/svg+xml" data="image.svg"></object>
复制代码


第三种,是直接把 svg 内容,通过<svg>标签嵌入至网页中,也就是说,svg 的数据内容直接是当前网页的一部分,浏览器是在加载当前网页时直接解释渲染的,而前面两种方式,则作为 svg 文件资源,由浏览器在加载解释当前页面时按文件所在 URL 进行网络下载。这是所谓的 inline svg 模式,或者称为内联的 svg。例如:

<!DOCTYPE html><html><body>
<svg height="210" width="400"> <path d="M150 0 L75 200 L225 200 Z" /> Sorry, your browser does not support inline SVG.</svg>
</body></html>
复制代码


第四种,在 CSS 中作为 background image 引入,例如:

#id {  background-image: url(image.svg);}
复制代码

这本质上和第一种方式相似。


上述四种方式的使用,各有优劣,例如:

  • inline 方式加载速度最快,而<object>方式则比较慢

  • 浏览器可以对<img>方式和<object>方式的 svg 图片资源作缓存。但 inline 模式下,浏览器则无法对 svg 作单独缓存

  • 前二者均可以通过 GZip 压缩(而且通常能达到 75%-85%的压缩率),但 inline 模式下 svg 数据和网页融为一体,就没办法单独压缩了

  • inline 方式下,svg 内容是当前网页里的标签下的数据内容,所以是可以被脚本动态处理的,可以基于用户操作行为作出动态的响应,交互性非常好;<object>方式下,也有一定程度的交互灵活性,但<img>方式则是完全不可能了

  • <img>和<object>方式下,svg 数据都是“封装”在各自的文件载体下,不用担心其中数据与当前网页中的其他内容冲突(例如里面的 ID、Class 和其他 svg 图形中 Element 的 ID、Class 重复),所以它们的 svg 数据是隔离的,修改维护都容易。但 inline 方式下,你必须保证每一个 svg 标签下的内容中的 Element 的 ID、Class 都是在当前网页下唯一的,否则渲染就会出问题

什么时候该用哪种方式?正常情况下,<img>方式是最简单直接、容易维护。但当你需要互动能力的时候,inline 是最佳选择。

使用 SVG 是否有安全风险

TL;DR 对于没时间兴趣关注本话题的读者,可以跳到下一节。简短的回答是:有 - 看你怎么用。但观点是:但不能因噎废食,在小程序里我们可以运用。


以下是关于 SVG 安全相关的详细内容。

首先,如上所述,SVG 是可以被脚本化的,例如:

<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg"><polygon id="triangle" points="0,0 0,50 50,0" fill="#009900" stroke="#004400"/><script type="text/javascript">alert('This app is probably vulnerable to XSS attacks!');</script></svg>
复制代码

看到<script>这个标签就全明白了,svg 不仅是矢量图,里面可以内嵌脚本。

XSS 攻击

这是如何发生的?只要你的 Web 应用允许用户上传、提交 svg 文件,内嵌在其中的恶意代码就可以妥妥的操作你应用页面里的 DOM,余下的就是“常规”XSS 攻击的事情。

HTML 注入

SVG 用 XML 语法和格式描述矢量,在 XML 中无法直接引用 HTML。为了满足这方面的应用需求,SVG 提供了一个叫 foreignObject 的元素,以便于开发者引入外部 XML namespace 下的元素。例如:

<?xml version= 1.0" standalone="no"?><!DOCTYPE SVg PUBLIC "-//WBC//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg" >    ‹rect x="10" y="1©" width="100" height="100" stroke="red" stroke-width="10" fill="white" />    <foreignObject class="node" x="46" y="22" width="200" height="300">        <body xmlns="http://www.w3.org/1999/xhtml">            <style>                h1 {color: blue}            </style›            <a-href="https://fortiguard.com">-Click-Here</a>            <h1>HTML - Injection for phishing</hi>        </body>    </foreignObject></svg>
复制代码


<body>标签下可以引入一个 XHTML 的 namespace,在标签下的的内容,都会被浏览器解析执行。此时,phishing(钓鱼)、CSRF(伪造跨站请求)、same-origin bypass(绕过同源限制)的骚操作都可以逐一发生。

恶意递归 XML Entity - “亿笑攻击”

所谓"Billion Laughs Attack",又称之为“XML 炸弹”(XML Bomb)或者“实体指数扩张攻击”(Entity Exponential Expansion Attack),是通过创建一系列递归的 XML 定义,在内存中产生上十亿的特定字符串,从而导致 DoS 攻击。原理是构造恶意的 XML 实体文件以耗尽服务器可用内存,因为许多 XML 解析器在解析 XML 文档时倾向于将它的整个结构保留在内存中,上亿的特定字符串占用巨量内存,使得解析器解析非常慢,并使得可用资源耗尽,从而造成拒绝服务攻击。


制作这些"lol"(Lots of Laugh)的 tricks 在这里:

//声明一个内部实体 -<!ENTITY entity-name "entity-value">
//声明一个外部实体 -<!ENTITY entity-name SYSTEM "URI/URL">
复制代码

写一个制造“笑气弹”的 svg:

<?xml version="1.0"?><!DOCTYPE lolz [ <!ENTITY lol "lol"> <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;"> <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;"> <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;"> <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;"> <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;"> <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;"> <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;"> <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">]><svg xmlns="http://www.w3.org/2000/svg">    <text x="0" y="10" font-size="20">&lol9;</text></svg>
复制代码


加载它,你的机器将“狂笑”一阵 - 大概 4 到 5 秒。原因?现在的浏览器都能处理这类攻击,自动“制止”继续 lol,但是通常需要 4-5 秒反应时间去判断和处理。

新型 DoS 攻击

所谓“道高一尺魔高一丈”,浏览器厂商有防,攻击者又有新的攻。这次是利用了 svg 中的这俩:xlink:href 这个 Attribute 和<use>这个 Element:

<defs>    <g id="a0">        <circle stroke="#000000" fill="#ffffff" fill-opacity="0.1" r="10 />    </g></defs><defs><g id="a1"><use x="0" y="10" xlink:href="#a0"/><use x="10" y="10" xlink:href="#a0"/><use x="20" y="10" xlink:href="#a0"/><use x="30" y="10" xlink:href="#a0"/><use x="40" y="10" xlink:href="#a0"/><g id="a2"><use x="0" y="10" xlink:href="#a1"/><use x="10" y="10" xlink:href="#a1"/><use x="20" y="10" xlink:href="#a1"/><use x="30" y="10" xlink:href="#a1"/><use x="40" y="10" xlink:href="#a2"/><g id="a3"><use ...>...
复制代码


层层递归的套路,直到浏览器崩溃。

小结

SVG 类型的内容资源,与其说是图片,还不如说是 HTML 的延伸扩展。所以,HTML 所能被利用的攻击手段,也可能都适用于 SVG。为了安全起见,原则上:

  • svg 资源不能以 object 甚至 iframe 的方式引入、加载

  • 禁止用户上传 svg

  • 管控通过未授权信任的链接加载外部的 svg 资源

  • 慎用<script>、<foreignObject>等比较强大但也有风险的标签

在 FinClip 小程序中能放心使用 SVG 吗

FinClip SDK是一个让任何 App“瞬间”获得运行小程序能力的安全沙箱。运行其中的小程序,相比一般的网页应用,获得更强的安全防护。

沙箱环境

SDK 启动的沙箱,提供一个纯 JavaScript 的解释执行环境,没有浏览器相关接口,无法操作 DOM、跳转。小程序业务逻辑相关的 JavaScript 代码均由沙箱创建的一个单独的线程去执行。界面渲染相关的任务,交由独立 Webview 线程负责,通过逻辑层代码去控制界面渲染。沙箱不支持动态载入脚本,XSS 攻击难以进行。

审核上架

FinClip 的服务器端提供了对小程序上下架的管控能力。经过审核的小程序才能上架;出现问题时,则可以一键下架。每个 FinClip 小程序需要事先设置通讯域名,小程序只能跟指定的域名与进行网络通信,包括普通 HTTPS 请求、上传文件、下载文件和 WebSocket 通信,参考框架-网络。这些通讯域名,也都必须要求通过备案。这些种种的限制和管理模式,都进一步保障安全。


开发者在开发小程序时引用的 SVG 资源,在小程序上架的源头可以进行检测审核。

控制 SVG 引入加载的方式

如前文所述,在标准浏览器中,起码有四种方式加载 SVG 资源(加上<iframe>和<embed>的话,实际上有 6 种可能,但这两种都不推荐使用,可以排除)。


从安全使用的角度看,把 svg 当作普通的图片资源,通过<img>引入,技术上支持,只要文件是自己或者可信的第三方提供。以<object>方式加载,则是可以引入风险的,因为它能触发对 svg 中上述一些脚本的执行。


inline(内联)方式,在小程序中是较为安全的方式,svg 内容变成了小程序页面代码的一部分,首先是开发者自行负责,而不是一个 URL 指向网上什么第三方的黑盒子资源,其次小程序审核上架的时候也可以检测其有无涉及上述有安全风险的标签使用方式。


FinClip目前对 svg 的支持,实际上合并了第三和第四种方式:即通过 CSS 中的 background image 加载 svg 图片,但是图片数据不是来自外部资源,而是 inline 生成的。

在 FinClip 小程序中 SVG 的打开方式

在小程序里成功使用 SVG 的诀窍在于这几处。

通过 CSS background image 加载,例如在你的 pages/index.fxml 中:

<view style="width:148px;height:148px;background:url('{{ svg_content_data }}') no-repeat center"></view>
复制代码


其中,svg_content_data 自然是由你的 pages/index.js 来负责产生了,它应该是长这个样子:

const svg = "data:image/svg+xml;base64," + [base64_encoding_of_svgdata];
复制代码


把这个 svg 数据绑定到所在页面的 data 对象中。


这里涉及到的两个关键内容:

Data URI scheme 它让我们把当前页面里生成的数据当作资源赋予给页面的标签,从而被浏览器渲染。以一个 svg 资源为例,

<img src="abc.svg" />
复制代码


是让渲染引擎在渲染当前的页面时,从同源的服务器上加载并渲染 abc.svg 图片。

如果 abc.svg 的内容是在当前页面里产生的呢?如何把生成的内容塞到 img 标签里去?

<img width="400" class="figure" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+C.....blah_blah_blah............CAgaW5rc2NhcGU6dHJhbnNmb3JtLWNlbnRlci14PSIwLjAxNzcwMzI1OSIKICAgICAgIGlua3NjYXBlOnRyYW5zZm9ybS1jZW50ZXIteT0iLTE5LjU0NzA3NCIgLz4KICA8L2c+Cjwvc3ZnPgo="></img>
复制代码


其中,"data:image/svg+xml;base64," 是我们用到的 data URI。

Base64 Encoding 上面 src 跟着的一大串字符串,是 base64 编码的 svg 内容,它在编码前的本尊应该是这样的:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 37 37" stroke="none">        <rect width="100%" height="100%" fill="#FFFFFF"/>        <path d="M4,4h1v1h-1z M5,4h1v1h-1z M6,4h1v1h-1z M7,4h1v1h-1z M8,4h1v1h-1z M9,4h1v1h-1z M10,4h1v1h-1z M14,4h1v1h-1z M15,4h1v1h-1z M18,4h1v1h-1z M23,4h1v1h-1z M26,4h1v1h-1z M27,4h1v1h-1z M28,4h1v1h-1z M29,4h1v1h-1z M30,4h1v1h-1z M31,4h1v1h-1z M32,4h1v1h-1z M4,5h1v1h-1z M10,5h1v1h-1z M13,5h1v1h-1z M17,5h1v1h-1z M19,5h1v1h-1z M22,5.......... fill="#000000"/></svg>
复制代码


这里有一个关键,"xmlns="http://www.w3.org/2000/svg" 的 XML namespace 的定义是必须的。


如何生成 base64 编码,在此提供一个函数供参考:

function convertBase64(svgxml) {    const data_uri = 'data:image/svg+xml;base64,';        svgxml = encodeURIComponent(svgxml);    const base64_encoded = btoa(unescape(svgxml));    return data_uri + base64_encoded;}
复制代码


实际例子可参考《FinClip小程序+Rust(五):用内联SVG实现二维码》。该文中,介绍了一个加密钱包的地址如何生成二维码并以 svg 方式展现。


本文首发于凡泰极客博客​​​​​​​,作者:F1n0Geek


用户头像

Speedoooo

关注

还未添加个人签名 2021.10.08 加入

还未添加个人简介

评论

发布
暂无评论
FinClip小程序里如何安全使用SVG_rust_Speedoooo_InfoQ写作社区