浏览器沙箱模式探究

网页插件或嵌入自定义图表都要运行不安全的第三方代码,本文探究如何让第三方代码在 web 网页中高效且安全运行。

Iframe 介绍

iframe 元素表示嵌套的浏览上下文。每个嵌入式浏览上下文都有其自己的会话历史记录和文档。

1
2
3
window === frame.contentWindow // false
window.document === frame.contentDocument // false
window.Array === frame.contentWindow.Array // false

注: 由于每个浏览上下文都是完整的文档环境,页面中的每个<iframe>都需要增加内存和其他计算资源。从理论上讲,您可以根据需要使用任意多个<iframe>,但请检查性能问题。

iframe问题:

  • iframe 阻塞 onload 事件:iframe会在主页面的onload之前加载。

  • 占用连接池

  • 耗性能:iframe 数量过多页面性能明显下降。

Shadow DOM 隔离 CSS 和 DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>shadowDom</title>
<style>
.chart-title {
color: blue !important;
}
</style>
</head>
<body>
<div id="custom_chart"></div>

<script>

var elementRef = document.getElementById("custom_chart");

// 1. 初始化一个 shadowDom
var shadow = elementRef.attachShadow({ mode: 'open' }); // open | closed(elementRef.shadowRoot = null)

// 2. 创建影子DOM结构
var title = document.createElement('h2');
title.textContent = '我是影子DOM';
title.classList.add('chart-title');
var content = document.createElement('div');
content.classList.add('chart-content');

// 3. 设置阴影DOM的样式
var style = document.createElement('style');
style.textContent = `
.chart-title {
color: red;
}
.chart-content {
width: 200px;
height: 100px;
background: silver;
}
`;

// 4. 设置 script 脚本
var script = document.createElement('script');
// script.src = 'https://cdnjs.cloudflare.com/ajax/libs/echarts/4.6.0/echarts.min.js';
script.textContent = `
var chart = 1;
console.log(window);
`;

// 4.将影子DOM附加到影子根
shadow.appendChild(style);
shadow.appendChild(script);
shadow.appendChild(title);
shadow.appendChild(content);
</script>

</body>
</html>

问题: shadowDom 并不隔离 js。

在主线程中如何隔离 js ?

eval(str)

1
2
3
eval('1+1;');
eval('function(){return 123;}');
eval('window.document.write("<script src=https://cdnjs.cloudflare.com/ajax/libs/echarts/4.6.0/echarts.min.js></script>")');

如何隐藏全局变量——Realm

使用示例:

1
2
3
4
5
6
7
8
9
let g = window; // outer global
let r = Realm.makeRootRealm(); // realm object

let f = r.evaluate("(function() { return 17 })");

f() === 17 // true

Reflect.getPrototypeOf(f) === g.Function.prototype // false
Reflect.getPrototypeOf(f) === r.global.Function.prototype // true

核心原理:

1
2
3
4
5
6
function simplifiedEval(scopeProxy, userCode) {
'use strict'
with (scopeProxy) {
eval(userCode)
}
}

with 语句【限制作用域】

with 语句接收的对象会添加到作用域链的前端并在代码执行完之后移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// https://www.tuicool.com/articles/FjiMFrY
// example 1
var obj = { name: 'xiaoming' };
function sayName() {
var name = 'xiaoqiang';
with(obj) {
console.log(name);
}
console.log(name);
}
sayName();

// example2
var obj = { name: 'xiaoming' };
function sayName() {
var name = 'xiaoqiang';
with(obj) {
name = 'xiaohua';
age = 30;
}
console.log(name);
console.log(age);
}
sayName();
console.log(obj.name);
console.log(obj.age);

Proxy 对象【访问拦截】

1
2
3
4
5
6
7
8
9
const scopeProxy = new Proxy(whitelist, {
get(target, prop) {
// here, target === whitelist
if (prop in target) {
return target[prop];
}
return undefined;
}
});

最终方案

Real + shadowDom

优点:

  1. n个自定义图表,仅需2个 iframe 解决,大大减少了iframe数量。
  2. 主线程中运行速度很快。
  3. 共享公共资源,节省内存和网络请求开支。
  4. 代码安全性比原来更高。

缺点和改进:

  1. 兼容性:Proxy IE11不支持,Edge支持,chrome 49以上支持。
  2. 加载多个第三方库冲突问题。
  3. 实践中的安全性性能问题仍有待进一步验证。

参考文档

如何安全的运行第三方 JavaScript 代码(上)?
如何安全的运行第三方 JavaScript 代码(中)?
如何安全的运行第三方 JavaScript 代码(下)?
原文-How to build a plugin system on the web and also sleep well at night
JS中的沙箱个人见解
iframe异步加载技术及性能
Realm-shim
Reflect