JavaScript 沙箱

概述
沙箱可(ke)以簡單的(de)(de)理(li)解為一個虛擬機(ji),是一個和宿(su)主(zhu)機(ji)隔離(li)的(de)(de)環(huan)境,在這個環(huan)境中去(qu)運行(xing)一些不(bu)受信任的(de)(de)代碼(ma)或者應用程序,防止(zhi)不(bu)安(an)全(quan)的(de)(de)代碼(ma)對(dui)系統造成(cheng)損害(hai)。
比(bi)如我(wo)們(men)現在知道某個(ge)(ge)應用是(shi)詐騙(pian)軟件(jian)或(huo)者病毒軟件(jian),但是(shi)我(wo)們(men)依舊(jiu)想要(yao)運行,想逆(ni)向分析(xi)他,那么我(wo)們(men)就可以選擇在電(dian)腦上安裝一個(ge)(ge)虛(xu)擬(ni)機,在這個(ge)(ge)虛(xu)擬(ni)機中,我(wo)們(men)將對(dui)攝(she)像(xiang)頭的訪(fang)問(wen)引導至一張靜態圖片或(huo)者視頻,將麥克(ke)風的訪(fang)問(wen)引導至一個(ge)(ge)事先錄制好(hao)的音頻文件(jian)中,通訊(xun)錄、應用列表(biao)我(wo)們(men)也(ye)提(ti)前(qian)做好(hao)“偽(wei)裝”提(ti)供給這個(ge)(ge)軟件(jian)。
上(shang)面(mian)所(suo)述的整個過程實際就是(shi)建立沙(sha)箱的一個過程,運行在這里面(mian)的應用所(suo)能訪(fang)問到的都是(shi)我們事先準備好的,他(ta)無法直(zhi)接訪(fang)問到我們的電腦環(huan)境,從而(er)保證了我們不受到惡(e)意攻擊。
計算機領域版的楚門的世界
當然(ran),電影也很好看(kan),推薦大家(jia)看(kan)看(kan)~
沙(sha)箱(xiang)機(ji)(ji)制的原理(li)非常好理(li)解(jie),適用性也很廣,在(zai)計算(suan)機(ji)(ji)領域中,沙(sha)箱(xiang)也存在(zai)很多種(zhong),本文(wen)僅介紹 JavaScript 中的沙(sha)箱(xiang)實(shi)現。
格局打開,不要僅僅把目光放在計算機領域,沙箱本質上就是 **讓你看到我想讓你看到的東西 **,詐騙實際也是(shi)遵循這種,很刑的。
應用場景
沙箱的應用場景十分(fen)廣泛,包(bao)括操作系統、網(wang)絡瀏覽器、移動應用程序等。
本文要介(jie)紹(shao)的 JS 沙箱通常(chang)(chang)是(shi)(shi)用(yong)于 Web 瀏覽器中的,限(xian)制(zhi)不受(shou)信(xin)任的代碼的訪(fang)問權限(xian),通常(chang)(chang)認為用(yong)戶自己編寫的代碼就(jiu)是(shi)(shi)不受(shou)信(xin)任的代碼。
服(fu)務器端也是可以使用 js 沙箱的(de)(de)(de)(de),用以執行不受信任的(de)(de)(de)(de)代碼(ma),比如在 leetcode 上做題的(de)(de)(de)(de)時(shi)候(hou),我(wo)們(men)提交的(de)(de)(de)(de)代碼(ma)是在服(fu)務器端執行的(de)(de)(de)(de),這是為了(le)防止用戶寫一些(xie)惡意的(de)(de)(de)(de)代碼(ma)突破權限,對服(fu)務器造成危害。
所以(yi),JS 沙箱的應用主要圍繞(rao)以(yi)下兩點:
- 安全:解析不受信的 js 文件,防止 XSS 等
- 應用程序隔離:限制代碼訪問相關的對象,如彈窗廣告
詳細(xi)展開可以(yi)有更多,但是(shi)大(da)體方向是(shi)這兩個。
實現
實現的思路上,可以(yi)大(da)(da)致(zhi)劃分(fen)為三大(da)(da)類:IFrame、JavaScript 語言特性、快照(zhao)
IFrame
特點 :瀏覽器(qi)支持的 HTML 元素,自(zi)帶沙(sha)箱隔離,能夠與主頁面通信。
缺點 :瀏覽器會為其單獨開啟一(yi)個子(zi)進程,有額外(wai)的(de)性能開銷(xiao)。不同瀏覽器對 sandbox 屬性的(de)支(zhi)持也有所不同
HTML元素,這個實際是瀏覽器支持的一種,實現會比較簡單,我們只需要使用對應的屬性 sandbox 即可,主要關注 allow-scripts 屬性, [1], 不過就個人使用情況而言,在使用 iframe 的時候,這個屬性(xing)基本都(dou)處于開(kai)啟狀態,不開(kai)啟的話,在嵌(qian)入一些(xie)網站的時候可能會顯示異常。
<iframe src="//bing.com" sandbox="allow-scripts" />
如上就是一個比較簡單的實現,我們禁用(yong)了(le)其他的能力,僅啟(qi)用(yong)了(le)腳(jiao)本執行的能力。
allow-scripts 僅允許執行腳本,但(dan)是無法創建(jian)彈窗(chuang)這類窗(chuang)口。
不過尤其注意,作為一種安(an)全(quan)機制,沙(sha)箱(xiang)(xiang)并不能保障絕(jue)對的安(an)全(quan),所(suo)以對于沙(sha)箱(xiang)(xiang)中(zhong)的內容(rong),我們還是需(xu)要(yao)保證加載(zai)的內容(rong)的可信度和安(an)全(quan)性,避免惡(e)意用戶突破(po)或繞過沙(sha)箱(xiang)(xiang)造(zao)成(cheng)攻擊,導致我們產生損失。
主要是存在跨站(zhan)腳本攻擊(XSS)的問題(ti),這里舉兩個(ge)例(li)子:
- 竊取敏感信息:如登錄憑據,通過過濾相關標簽或者編碼來規避。
- 點擊劫持:釣魚網站,通常不是對網站的危害,而是對用戶的,采用禁止網站被嵌入(
X-Frame-Options或Content-Security-Policy)來規避
一般來說(shuo),我(wo)們在 iframe 中都會訪問受信的(de)站(zhan)點(dian)(dian),即使是(shi)彈(dan)窗廣告(gao),不(bu)過對于(yu)廣告(gao),我(wo)們通常會限(xian)制一些操作,比如(ru)不(bu)允(yun)許運行腳本。如(ru)果不(bu)加以限(xian)制,他(ta)可能(neng)不(bu)會直接危害到我(wo)們的(de)站(zhan)點(dian)(dian)安全,但是(shi)可能(neng)有用(yong)戶(hu)信任下(xia)降及(ji)影響(xiang)品牌聲譽的(de)問題(ti),當有更好的(de)站(zhan)點(dian)(dian)作為替(ti)代的(de)時(shi)候,那用(yong)戶(hu)則會棄之如(ru)敝履。
目前這種彈窗廣告你(ni)已(yi)經(jing)很少能在正規的網站上看到了(le),各個(ge)大廠更傾向于在信息(xi)流中加入廣告。
JavaScript 語言特性
特點:各個瀏覽器表現基本(ben)一致
缺點:性能表現與代碼實現的優(you)劣相關
前文說過,沙箱本質(zhi)是(shi)一種安全機制,是(shi)為了_** 限制第三方不受信(xin)任的代碼對系統內容的訪問(wen) **_,所以結合我(wo)(wo)們(men)對 JavaScript 語言的了解,我(wo)(wo)們(men)可(ke)以考(kao)慮(lv)作用域來(lai)限制訪問(wen)系統級(ji)變(bian)量。
作用域
目前市面上的編程語言,基本都有作用域的概念:在程序中定義變量的可見性和訪問范圍。 它直接決(jue)定了(le)變量(liang)的生(sheng)命周期和可以訪問變量(liang)的代碼片段,一般作用域(yu)[2]包括:
- 全局作用域(Global Scope):當前程序可以在任意位置處訪問的變量或函數,如 window。
- 模塊作用域(Module Scope):即 import 和 export,通常認為一個文件是一個模塊,在文件內定義的變量或函數都是該模塊私有的,如需在外部使用,則需要 export。
- 函數作用域(Function Scope):由函數創建的作用域,在 JavaScript 中,創建函數會為我們創建一個獨立的作用域,在 es6 之前沒有 es Module 規范時,我們使用 Function 幫助我們創建獨立的作用域來實現模塊化,即 umd 和 amd。
- 塊級作用域(Block Scope):es6 中引入,用一對花括號括起來的代碼塊,只對 let 和 const 聲明有效。var 聲明無效。
看到這(zhe)里,不(bu)難看出,作用(yong)域(yu)實(shi)(shi)際就(jiu)是一(yi)個(ge)天然(ran)的沙盒,我們可(ke)以這(zhe)樣實(shi)(shi)現:
window; // 瀏覽器的 window 對象
window.app = 2; // 增加一個 app 字段,并將其賦值為 2
function execCode(code: string) {
const window = null;
;
}
const code = 'window.app = 1';
execCode(code); // 將 window.app 的值修改為 1,執行結果:Uncaught TypeError: Cannot set properties of null (setting 'app')
可(ke)以(yi)看到,此(ci)時執(zhi)行第三方代(dai)碼的時候,這些代(dai)碼是無法(fa)訪問我們的 window 對象(xiang)的,從而保(bao)證了我們的 window 對象(xiang)的安全。
但實際這并不是真正的安全,我們依舊有辦法能夠繞過他,比如,當我們全局定義了一個函數 updateApp(app) 的時候,我們(men)在這里實際可以通過調用這個函數(shu)的方(fang)式來繞過我們(men)作用域(yu)的限制:
function updateApp(app: number) {
window.app = app;
}
execCode('updateApp(1)'); // 執行成功,window.app 此時為 1
所以我們又要重申一遍:作為一種安全機制,沙箱并不能保障絕對的安全
這里沒(mei)有考慮 with 關鍵字,因為(wei)他已(yi)(yi)經(jing)從 es5 開始的嚴格(ge)模式下就已(yi)(yi)經(jing)被禁用了,而現在由于我們使(shi)用的框(kuang)架默認是(shi)以嚴格(ge)模式執行的,所以可(ke)以說 with 關鍵字其實已(yi)(yi)經(jing)處于不(bu)(bu)可(ke)用的狀(zhuang)態了。新項目(mu)已(yi)(yi)經(jing)不(bu)(bu)建議使(shi)用,但是(shi)老項目(mu)還在用的話,那就保持現狀(zhuang)吧。
不過對于這種方式實現的沙盒,我們實際可以進一步優化,引入 JavaScript 中的 new Function [3] 構造函數來執行代碼,避免一些簡單的安全(quan)問題:
function execCode(code) {
const func = new Function('window', code);
func(null);
}
此時再執行時,你會發現,updateApp(1) 執行會報錯 ReferenceError: updateApp is not defined ,代碼字符(fu)串編(bian)譯后(hou)無法訪問我們的(de) updateApp 函數了。
因為我們在這里構造了一個類似于 function (window) { window.app = 1 } 的函數,并(bing)在后續執行調用動作(zuo)。
eval 能夠(gou)訪(fang)問本地作(zuo)用域(yu),new Function 則只能訪(fang)問全局(ju)變量和自己的(de)局(ju)部變量,同時(shi)其構造器創建時(shi)所在的(de)作(zuo)用域(yu)的(de)內容(rong)是(shi)無法(fa)訪(fang)問的(de)。
Proxy 代理
看上去,Proxy 是(shi)一(yi)個(ge)比(bi)較復雜(za)的內容,但實(shi)際上他本質上就(jiu)是(shi)一(yi)個(ge)攔(lan)截器,在(zai)訪問(wen)目標內容之(zhi)前(qian),需要先經過 Proxy 幫忙去通知。
一(yi)個更貼近現(xian)實的例子,Proxy = 租(zu)房中介,你(ni)想要(yao)租(zu)房,就需要(yao)通過中介去介紹。
如果你繞(rao)過中介直接和房東交易(yi),當然也是(shi)可(ke)行(xing)的,因為原始的交易(yi)對象是(shi)你已知的。
Proxy 的(de)一個簡單示例:
const windowProxy = new Proxy(window, {
get(obj, propKey) {
if (propKey === 'test') {
return 'hello world';
}
return obj[propKey];
}
})
windowProxy.test; // hello world,通過中介交易
window.test; // undefined,繞過中介直接和房東交易
通過上面這個簡單的例子,我們可以很輕松的看出,我們在創建出來的 Proxy 對象中限制了他訪問 test 屬性(xing)的(de)內容,這實際(ji)上是在(zai)做我們一(yi)開始(shi)說的(de),偽裝應用(yong)程(cheng)序(xu)所(suo)需要的(de)內容(變量等),提(ti)供(gong)給應用(yong)程(cheng)序(xu)使用(yong),從而實現(xian)對惡意(yi)攻擊的(de)防范。至于(yu)運行(xing)代碼(ma)?那不好意(yi)思,Proxy 本身是無法(fa)像(xiang) eval 和 new Function 一(yi)樣去運行(xing)一(yi)段代碼(ma)的(de)。
不過 Proxy 方式(shi)創(chuang)建沙(sha)箱也(ye)需要注意:
- proxy 默認只會代理一級對象:也就是說,當訪問的對象是
{ a: { b: { c: 1 } } }這種,用戶訪問proxy.a.b.c其實操作的就是原對象。
基于快照的沙箱
快照(Snapshot),就是(shi)存(cun)儲某(mou)一時刻相關(guan)數(shu)據的副本。
基于快照的(de)沙(sha)箱(xiang),顧名(ming)思義(yi),就是在程序運(yun)行(xing)的(de)某一(yi)個(ge)時(shi)刻,或者(zhe)執行(xing)某一(yi)個(ge)操作時(shi)保存(cun)當(dang)前運(yun)行(xing)環境(jing)(jing)副(fu)本,再后續的(de)某一(yi)個(ge)時(shi)刻恢復原運(yun)行(xing)環境(jing)(jing),從而(er)實現沙(sha)箱(xiang)機制。
通(tong)常我(wo)們是(shi)將副本用于(yu)操(cao)作,操(cao)作結束時(shi),將副本的更新寫入原始運行環(huan)境,舉個例(li)子(zi):
const snapshotSandbox = {
original: null,
copied: null,
beforeAction: (obj, dangerKeys) => {
// 保留原始副本
this.original = obj;
const snapshot = {};
// 記錄當前信息,做一次快照
for (const key of Object.keys(obj)) {
if (dangerKeys.includes(key)) {
// 對于敏感信息,不提供或提供加密信息
continue;
}
snapshot[key] = obj[key];
}
console.log(snapshot);
this.copied = snapshot;
return this.copied;
},
afterAction: () => {
// 將操作結果更新到原始副本中
for (const key of Object.keys(this.copied)) {
this.original[key] = this.copied[key];
}
}
}
function getUserId(user) {
const id = user.id;
const name = user.name;
console.log('id', id, 'name', name);
// ajax('用于第三方不安全平臺登記認證');
}
const code = getUserId.toString();
const base = {
id: 'xxxx',
name: '一個人',
age: '12',
phoneNo: '1333333333333'
};
// 1. 現在假設有一個危險操作被注入到頁面上會使用用戶的 ID 信息(身份證)
const userInfo = snapshotSandbox.beforeAction(base, ['id', 'phoneNo']);
// 2. 做壞事
;
// 3. 把更新的內容寫回原來的對象
snapshotSandbox.afterAction();
console.log('使用的 userInfo 信息', userInfo);
console.log('最終操作后的 userInfo 信息', base);
console.log('這兩個信息理論上不是同一個對象', userInfo !== base)
這個(ge)實現(xian)僅(jin)僅(jin)是(shi)其中一(yi)種方(fang)式,在微前(qian)端中,基于快照(zhao)的(de)實現(xian)在以前(qian)也是(shi)一(yi)種流行的(de)版本,目的(de)是(shi)為了(le)隔離(li)不同子應用,現(xian)在多數基于 Proxy 來實現(xian)了(le),但快照(zhao)沙箱依舊是(shi)作(zuo)為一(yi)種降級方(fang)案(an)去兼容老舊的(de)瀏覽器。
最后
我們從應用場景的角度分析,JS 沙箱聚焦于 “安全防護” 與 “應用隔離” 這兩大核心需求:
在 Web 瀏覽器端,JS 沙箱能夠限制用戶自行編寫的代碼作用范圍。例如,它可實現攔截彈窗廣告、抵御 XSS 攻擊等功能,避免不可信的 JS 文件對頁面正常運行造成干擾。而在服務器端,如 LeetCode 代碼提交、在線編輯器等,沙箱可以提供有力保障。它能夠防止用戶提交的惡意代碼突破權限限制,從而避免服務器配置被篡改以及數據被竊取的風險。
盡管(guan) Web 瀏覽器(qi)(qi)端(duan)(duan)和服務器(qi)(qi)端(duan)(duan)的部署(shu)環境存在(zai)差(cha)異,然而它(ta)們的最終目標是一致的:即,確保代(dai)碼在(zai)可控范圍內(nei)運行,降(jiang)低風(feng)險擴散的可能性。
參考文章
[1]:
[2]:
[3]:
本文首發于,公眾號訂閱請(qing)關注:

