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

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

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

网上零零散散有一些关于在小程序中如何使用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方式展现。

可私有化的小程序生态管理系统 - FinClip

立即了解