Dieses Mal werde ich Ihnen die praktische Verwendung der Anwendungs-Sandbox-Umgebung vorstellen. Was sind die Vorsichtsmaßnahmen für die tatsächliche Verwendung der Anwendungs-Sandbox-Umgebung? Das Folgende ist ein praktischer Fall.
Welche Szenarien gibt es für die dynamische Ausführung von Skripten?
Wir hoffen, Benutzern die Möglichkeit zu geben, benutzerdefinierte Logik einzufügen, z. B. VBA in Microsoft Office, Lua-Skripte in einigen Spielen und FireFoxs „Grease Monkey Script“. ihre Fantasie zu nutzen, um einige lustige und nützliche Dinge innerhalb kontrollierbaren Umfangs und mit kontrollierbarer Autorität zu tun und so die Fähigkeiten zu erweitern, um den individuellen Bedürfnissen der Benutzer gerecht zu werden.
Bei den meisten davon handelt es sich um Client-Programme, für die in einigen Online-Systemen und -Produkten häufig ähnliche Anforderungen gelten. Tatsächlich bieten viele Online-Anwendungen auch die Möglichkeit, Skripte anzupassen, wie z. B. Google Apps Script in Docs Mit JavaScript können Sie einige sehr nützliche Dinge tun, z. B. Code als Reaktion auf Dokumentöffnungsereignisse oder Zelländerungsereignisse ausführen, benutzerdefinierte Tabellenfunktionen für Formeln erstellen und vieles mehr.
Im Gegensatz zu Client-Anwendungen, die „auf dem Computer des Benutzers“ ausgeführt werden, können die benutzerdefinierten Skripte des Benutzers normalerweise nur den Benutzer selbst betreffen. Bei Online-Anwendungen oder -Diensten werden einige Situationen noch komplizierter, z. B. „Sicherheit“. Das „benutzerdefinierte Skript“ des Benutzers muss streng eingeschränkt und isoliert sein, d. h. es darf das Hostprogramm oder andere Benutzer nicht beeinträchtigen.
Safeify ist ein Modul für Nodejs-Anwendungen, das zur sicheren Ausführung benutzerdefinierter, nicht vertrauenswürdiger Skripte verwendet wird.
Wie führt man dynamische Skripte sicher aus?
Werfen wir zunächst einen Blick darauf, wie man einen Codeabschnitt in einem JavaScript-Programm dynamisch ausführt? Zum Beispiel die berühmte eval
eval('1+2')
Der obige Code wurde reibungslos und ohne Probleme ausgeführt. eval ist ein Funktionsattribut des Global Bei der Ausführung verfügt der Code über die gleichen Berechtigungen wie andere normale Codes im Anwendungsprozess. Er kann im „Ausführungskontext“ auch auf alle „globalen Variablen“ zugreifen .
Schauen wir uns Functionn noch einmal an. Mit dem Funktionskonstruktor können wir eine Funktion dynamisch erstellen und dann ausführen.
const sum = new Function('m', 'n', 'return m + n'); console.log(sum(1, 2));
Sie wird auch reibungslos ausgeführt, indem die vom Funktionskonstruktor generierte Funktion verwendet wird werden nicht in dem Kontext erstellt, in dem sie erstellt wurden, sondern werden im Allgemeinen im globalen Bereich erstellt. Beim Ausführen einer Funktion können Sie nur auf Ihre eigenen lokalen Variablen und globalen Variablen zugreifen, nicht jedoch auf den Bereich des Kontexts, der vom aufgerufenen Funktionskonstruktor generiert wird. So wie einer auf dem Boden steht und der andere auf einem dünnen Blatt Papier steht, gibt es in dieser Szene fast keinen Unterschied zwischen Vorgesetztem und Unterlegenem.
In Kombination mit der neuen Proxy-Funktion von ES6 kann es sicherer sein
function evalute(code,sandbox) { sandbox = sandbox || Object.create(null); const fn = new Function('sandbox', `with(sandbox){return ($[code])}`); const proxy = new Proxy(sandbox, { has(target, key) { // 让动态执行的代码认为属性已存在 return true; } }); return fn(proxy); } evalute('1+2') // 3 evalute('console.log(1)') // Cannot read property 'log' of undefined
Wir wissen, dass der Bereich unabhängig von der Auswertung oder Funktion Schicht für Schicht durchsucht wird, wenn er nicht gefunden wird. Es wird weiterhin global verwendet. Das Prinzip der Verwendung von Proxy besteht darin, den ausgeführten Code in Sandobx finden zu lassen, um den Zweck der „Anti-Escape“ zu erreichen.
Im Browser können Sie auch iframe verwenden, um eine sichere Isolationsumgebung für die erneute Übertragung zu erstellen. Dieser Artikel konzentriert sich auch auf Node.js und wird hier nicht zu ausführlich behandelt.
Gibt es in Node.js noch andere Optionen?
Vielleicht haben Sie bereits an VM gedacht, bevor Sie dies gesehen haben. Es handelt sich um ein integriertes Modul, das standardmäßig von Node.js bereitgestellt wird. Das VM-Modul bietet eine Reihe von APIs zum Kompilieren und Ausführen in der V8-Umgebung . Führen Sie den Code aus. JavaScript-Code kann sofort kompiliert und ausgeführt oder kompiliert, gespeichert und dann ausgeführt werden.
const vm = require('vm'); const script = new vm.Script('m + n'); const sandbox = { m: 1, n: 2 }; const context = new vm.createContext(sandbox); script.runInContext(context);
Führen Sie den obigen Code aus, um das Ergebnis 3 zu erhalten. Gleichzeitig kann vm.Script auch die „maximale Anzahl von Millisekunden“ angeben, die der Code ausführen soll, wenn die angegebene Zeit die angegebene Zeit überschreitet , die Ausführung wird abgebrochen und eine Ausnahme ausgelöst.
try { const script = new vm.Script('while(true){}',{ timeout: 50 }); .... } catch (err){ //打印超时的 log console.log(err.message); }
Die Ausführung des obigen Skripts schlägt fehl und es wird eine Ausnahme ausgelöst. Es ist zu beachten, dass die Timeout-Option von vm.Script „nur für synchrone Generierung“ gültig ist, jedoch nicht die Zeit asynchroner Aufrufe einschließt. Beispiel:
const script = new vm.Script('setTimeout(()=>{},2000)',{ timeout: 50 }); ....
Der obige Code löst keine aus Ausnahme nach 50 ms, da die synchrone Ausführung des Codes über 50 ms definitiv abgeschlossen ist und die von setTimeout verwendete Zeit dies nicht berücksichtigt. Das heißt, das VM-Modul hat keine Möglichkeit, die Ausführungszeit von asynchronem Code direkt zu begrenzen. Wir können auch keinen zusätzlichen Timer zur Überprüfung des Timeouts verwenden, da es keine Möglichkeit gibt, die ausgeführte VM nach der Überprüfung abzubrechen.
Darüber hinaus scheint vm.runInContext in Node.js die Codeausführungsumgebung zu isolieren, tatsächlich ist es jedoch leicht, „zu entkommen“.
const vm = require('vm'); const sandbox = {}; const script = new vm.Script('this.constructor.constructor("return process")().exit()'); const context = vm.createContext(sandbox); script.runInContext(context);
执行上边的代码,宿主程序立即就会「退出」,sandbox 是在 VM 之外的环境创建的,需 VM 中的代码的 this 指向的也是 sandbox,那么
//this.constructor 就是外所的 Object 构建函数 const ObjConstructor = this.constructor; //ObjConstructor 的 constructor 就是外包的 Function const Function = ObjConstructor.constructor; //创建一个函数,并执行它,返回全局 process 全局对象 const process = (new Function('return process'))(); //退出当前进程 process.exit();
没有人愿意用户一段脚本就能让应用挂掉吧。除了退出进程序之外,实际上还能干更多的事情。
有个简单的方法就能避免通过 this.constructor 拿到 process,如下:
const vm = require('vm'); //创建一外无 proto 的空白对象作为 sandbox const sandbox = Object.create(null); const script = new vm.Script('...'); const context = vm.createContext(sandbox); script.runInContext(context);
但还是有风险的,由于 JavaScript 本身的动态的特点,各种黑魔法防不胜防。事实 Node.js 的官方文档中也提到 VM 当做一个安全的沙箱去执行任意非信任的代码。
有哪些做了进一步工作的社区模块?
在社区中有一些开源的模块用于运行不信任代码,例如 sandbox、vm2、jailed 等。相比较而言 vm2 对各方面做了更多的安全工作,相对安全些。
从 vm2 的官方 READM 中可以看到,它基于 Node.js 内建的 VM 模块,来建立基础的沙箱环境,然后同时使用上了文介绍过的 ES6 的 Proxy 技术来防止沙箱脚本逃逸。
用同样的测试代码来试试 vm2
const { VM } = require('vm2'); new VM().run('this.constructor.constructor("return process")().exit()');
如上代码,并没有成功结束掉宿主程序,vm2 官方 REAME 中说「vm2 是一个沙盒,可以在 Node.js 中按全的执行不受信任的代码」。
然而,事实上我们还是可以干一些「坏」事情,比如:
const { VM } = require('vm2'); const vm = new VM({ timeout: 1000, sandbox: {}}); vm.run('new Promise(()=>{})');
上边的代码将永远不会执行结束,如同 Node.js 内建模块一样 vm2 的 timeout 对异步操作是无效的。同时,vm2 也不能额外通过一个 timer 去检查超时,因为它也没有办法将执行中的 vm 终止掉。这会一点点耗费完服务器的资源,让你的应用挂掉。
那么或许你会想,我们能不能在上边的 sandbox 中放一个假的 Promise 从而禁掉 Promise 呢?答案是能提供一个「假」的 Promise,但却没有办法完成禁掉 Promise,比如
const { VM } = require('vm2'); const vm = new VM({ timeout: 1000, sandbox: { Promise: function(){}} }); vm.run('Promise = (async function(){})().constructor;new Promise(()=>{});');
可以看到通过一行 Promise = (async function(){})().constructor 就可以轻松再次拿到 Promise 了。从另一个层面来看,况且或许有时我们还想让自定义脚本支持异步处理呢。
如何建立一个更安全一些的沙箱?
通过上文的探究,我们并没有找到一个完美的方案在 Node.js 建立安全的隔离的沙箱。其中 vm2 做了不少处理,相对来讲算是较安全的方案了,但问题也很明显,比如异步不能检查超时的问题、和宿主程序在相同进程的问题。
没有进程隔离时,通过 VM 创建的 sanbox 大体是这样的
那么,我们是不是可以尝试,将非受信代码,通过 vm2 这个模块隔离在一个独立的进程中执行呢?然后,执行超时时,直接将隔离的进程干掉,但这里我们需要考虑如下几个问题
通过进程池统调度管理沙箱进程
如果来一个执行任务,创建一个进程,用完销毁,仅处理进程的开销就已经稍大了,并且也不能不设限的开新进程和宿主应用抢资源,那么,需要建一个进程池,所有任务到来会创建一个 Script 实例,先进入一个 pending 队列,然后直接将 script 实例的 defer 对象返回,调用处就能 await 执行结果了,然后由 sandbox master 根据工程进程的空闲程序来调度执行,master 会将 script 的执行信息,包括重要的 ScriptId,发送给空闲的 worker,worker 执行完成后会将「结果 + script 信息」回传给 master,master 通过 ScriptId 识别是哪个脚本执行完毕了,就是结果进行 resolve 或 reject 处理。
这样,通过「进程池」即能降低「进程来回创建和销毁的开销」,也能确保不过度抢占宿主资源,同时,在异步操作超时,还能将工程进程直接杀掉,同时,master 将发现一个工程进程挂掉,会立即创建替补进程。
处理的数据和结果,还有公开给沙箱的方法
进程间如何通讯,需要「动态代码」处理数据可以直接序列化后通过 IPC 发送给隔离 Sandbox 进程,执行结果一样经过序列化通过 IPC 传输。
其中,如果想法公开一个方法给 sandbox,因为不在一个进程,并不能方便的将一个方案的引用传递给 sandbox。我们可以将宿主的方法,在传递给 sandbox worker 之类做一下处理,转换为一个「描述对象」,包括了允许 sandbox 调用的方法信息,然后将信息,如同其它数据一样发送给 worker 进程,worker 收到数据后,识出来所「方法描述对象」,然后在 worker 进程中的 sandbox 对象上建立代理方法,代理方法同样通过 IPC 和 master 通讯。
最终,我们建立了一个大约这样的「沙箱环境」
如此这般处理起来是不是感觉很麻烦?但我们就有了一个更加安全一些的沙箱环境了,这些处理。笔者已经基于 TypeScript 编写,并封装为一个独立的模块 Safeify。
GitHub: https://github.com/Houfeng/safeify,欢迎 Star & Issues
最后,简单介绍一下 Safeify 如何使用,通过如下命令安装
npm i safeify --save
在应用中使用,还是比较简单的,如下代码(TypeScript 中类似)
import { Safeify } from './Safeify'; const safeVm = new Safeify({ timeout: 50, //超时时间,默认 50ms asyncTimeout: 500, //包含异步操作的超时时间,默认 500ms quantity: 4 //沙箱进程数量,默认同 CPU 核数 }); const context = { a: 1, b: 2, add(a, b) { return a + b; } }; const rs = await safeVm.run(`return add(a,b)`, context); console.log('result',rs);
相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!
推荐阅读:
Das obige ist der detaillierte Inhalt vonPraktische Nutzung der Anwendungs-Sandbox-Umgebung. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!