JavaScript Sandboxについての簡単な説明

JavaScript Sandboxについての簡単な説明

序文:

サンドボックスといえば、私たちの頭には反射的に上の写真が思い浮かび、すぐに興味がわいてくるかもしれませんが、残念ながらこの記事には「Minecraft」(昔のカバーパーティー)は関係ありません。次の記事では、「Browser World」のサンドボックスを徐々に紹介していきます。

1. サンドボックスとは何ですか?

コンピュータ セキュリティにおいて、 Sandboxは実行中のプログラムを隔離するために使用されるセキュリティ メカニズムです。通常、テストされていないプログラムや信頼されていないプログラムやコードを実行するために使用されます。実行されるプログラムに対して独立した実行環境を作成し、内部プログラムの実行が外部プログラムの動作に影響を与えないようにします。

たとえば、次のシナリオにはサンドボックスの抽象的な概念が関係しています。

  • 開発したページ プログラムは、ブラウザー内で実行されます。プログラムで変更できるのは、ブラウザーが変更を許可しているインターフェースの部分のみです。このスクリプトを通じてブラウザー外部の状態に影響を与えることはできません。このシナリオでは、ブラウザー自体がサンドボックスです。
  • ブラウザ内の各タブは独立した Web ページを実行し、各タブは互いに影響を及ぼしません。このタブはサンドボックスです。
  • ......

2. サンドボックスの適用シナリオは何ですか?

上記では、比較的マクロなサンドボックス シナリオをいくつか紹介しましたが、実際には、日常的な開発では、このようなメカニズムの適用を必要とするシナリオが数多くあります。

  • JSONPリクエストによって返された文字列を実行したり、不明なサードパーティのJSライブラリを導入したりする場合は、これらのコードを実行するためにサンドボックスを作成する必要がある場合があります。
  • Vueテンプレート式の計算はサンドボックスで実行されます。テンプレート文字列内の式は、一部のグローバルオブジェクトのみを取得できます。これは公式ドキュメントに記載されています。詳細については、ソースコードを参照してください。

  • CodeSanboxなどのオンライン コード エディターは、スクリプトを実行するときにプログラムをサンドボックスに配置し、プログラムがメイン ページにアクセスしたり、メイン ページに影響を与えたりすることを防ぎます。
  • 多くのアプリケーションはPluginメカニズムを提供しており、開発者は独自のプラグインを作成して特定のカスタム機能を実装できます。プラグインを開発した学生は、プラグインの開発には多くの制約があることを知っておく必要があります。これらのアプリケーションは、プラグインを実行するときに、ホスト プログラムによって設定された動作ルールに従う必要があります。プラグインの動作環境とルールはサンドボックスです。たとえば、次の図はFigmaプラグインがどのように動作するかを示しています。

つまり、信頼できないサードパーティのコードに遭遇した場合でも、サンドボックスを使用してコードを分離し、外部プログラムの安定した動作を確保することができます。信頼できないコードが何の処理もせずに実行されると、フロントエンドにおける最も明らかな副作用/損害は、グローバルwindow状態の汚染と改ざんであり、メインページ機能に影響を与え、さらには XSS 攻撃を受けることになります。

// サブアプリケーションコード window.location.href = 'www.diaoyu.com'

Object.prototype.toString = () => {

    console.log('あなたは愚か者です:)')

  }

document.querySelectorAll('div').forEach(node ​​=> node.classList.add('hhh'))

リクエストを送信します(ドキュメント.cookie)

...

3. JSサンドボックスの実装方法

サンドボックスを実装するには、実際にはプログラム実行メカニズムを開発する必要があり、このメカニズムの動作により、サンドボックス内のプログラムの動作は外部プログラムの動作に影響を与えません。

3.1 最もシンプルなサンドボックス

この効果を達成するための最も直接的なアイデアは、プログラム内でアクセスされるすべての変数が、グローバル実行環境から値を取得するのではなく、信頼できるまたは自律的なコンテキスト環境から取得されることです。次に、すべての変数が信頼できるコンテキスト環境からアクセスされるようにするには、

プログラムを実行するためのスコープを構築する必要があります。

//実行コンテキストオブジェクト const ctx = 
    関数: 変数 => {
        console.log(変数)
    },
    フー: 'フー'
}

// 最も単純なサンドボックス関数poorestSandbox(code, ctx) {
    eval(code) // プログラムを実行するための関数スコープを構築します}

// 実行するプログラム const code = `
    ctx.foo = 'バー'
    ctx.func(ctx.foo)
`

povertySandbox(code, ctx) // バー

このようなサンドボックスでは、ソース プログラムが変数を取得するときに実行コンテキスト オブジェクトのプレフィックスを追加する必要がありますが、第三者の動作を制御する方法がないため、これは明らかに非常に不合理です。このプレフィックスを削除する方法はありますか?

3.2 非常にシンプルなサンドボックス(あり)

withステートメントを使用すると、このプレフィックスを削除することができます。 with 、スコープ チェーンの先頭に新しいスコープを追加します。 このスコープの変数オブジェクトは、 withによって渡されたオブジェクトに追加されます。 したがって、外部環境と比較して、内部コードは変数を検索するときにこのオブジェクトの検索を優先します。

//実行コンテキストオブジェクト const ctx = {
    関数: 変数 => {
        console.log(変数)
    },
    フー: 'フー'
}

// 非常に貧弱なサンドボックス function veryPoorSandbox(code, ctx) {
    with(ctx) { // 追加
        評価(コード)
    }
}

// 実行するプログラム const code = `
    foo = 'バー'
    関数foo
`

veryPoorSandbox(code, ctx) // バー

これにより、実行中のプログラム内の変数が、外部実行環境の前にサンドボックスによって提供されるコンテキストで検索されるという効果が得られます。

問題は、提供されたコンテキスト オブジェクトで変数が見つからない場合でも、コードはスコープ チェーンをレイヤーごとに検索し続けることです。このようなサンドボックスでは、内部コードの実行を制御できません。サンドボックス内のコードでは、手動で提供されたコンテキスト オブジェクト内の変数のみを検索し、コンテキスト オブジェクト内に変数が存在しない場合はエラーを報告するか、 undefinedを返すようにします。

3.3 それほど単純ではないサンドボックス(プロキシあり)

上記の問題を解決するために、 ES2015の新機能であるProxyを使用します。Proxy Proxyオブジェクトをプロキシし、オブジェクトの基本的な操作を傍受して定義することができます。

Proxyの get メソッドと set メソッドは、プロキシ オブジェクトに既に存在するプロパティのみをインターセプトできます。これらの 2 つのフックは、プロキシ オブジェクトに存在しないプロパティを認識しません。したがって、ここではProxy.has()を使用して、with コード ブロック内の任意の変数へのアクセスをインターセプトし、ホワイトリストを設定します。ホワイトリスト内の変数には、スコープ チェーンを使用して通常どおりアクセスできます。ホワイトリストにない変数は、サンドボックスによって管理されるコンテキスト オブジェクト内に存在するかどうかを引き続き判断します。存在する場合は、通常どおりアクセスされます。存在しない場合は、エラーが直接報告されます。

has はwithコード ブロック内のすべての変数アクセスをインターセプトし、実行されたコード ブロック内のプログラムのみを監視したいため、手動コード実行の形式も変換する必要があります。

// 実行するコードをラップする with を構築し、with コードブロックの関数インスタンスを返します function withedYourCode(code) {
  コード = 'with(globalObj) {' + コード + '}'
  新しい関数('globalObj'、コード)を返します
}


// アクセスできるグローバルスコープのホワイトリスト const access_white_list = ['Math', 'Date']


// 実行するプログラム const code = `
    Math.random()
    location.href = 'xxx'
    関数foo
`

//実行コンテキストオブジェクト const ctx = {
    関数: 変数 => {
        console.log(変数)
    },
    フー: 'フー'
}

// 実行コンテキストオブジェクトのプロキシオブジェクト const ctxProxy = new Proxy(ctx, {
    has: (target, prop) => { // has は with コード ブロック内の任意のプロパティへのアクセスをインターセプトできます if (access_white_list.includes(prop)) { // アクセス可能なホワイトリストでは、上方向に検索を続けることができます return target.hasOwnProperty(prop)
      }

      ターゲットが独自のプロパティを持っている場合、
          throw new Error(`無効な式 - ${prop}! これは実行できません!`)
      }

      真を返す
    }
})

// それほど貧弱ではないサンドボックス function littlePoorSandbox(code, ctx) {

    withedYourCode(code).call(ctx, ctx) // これを手動で構築されたグローバル プロキシ オブジェクトにポイントします}


littlePoorSandbox(コード、ctxProxy)

// キャッチされないエラー: 無効な式 - 場所! それはできません!

この時点で、比較的単純なシナリオの多くはカバーできます ( eg: Vueのテンプレート文字列)。しかし、 CodeSanboxのようなwebエディターを実装したい場合はどうでしょうか?このようなエディターでは、メイン ページに影響を与えることなく、 documentlocationなどのグローバル変数を自由に使用できます。

これにより、別の疑問が生じます。サブルーチンが外部のグローバル状態に影響を与えずにすべてのグローバル オブジェクトを使用できるようにするにはどうすればよいでしょうか。

3.4 ナチュラルな高品質サンドボックス(iframe)

上記の質問を聞いたとき、私はすぐに自分を専門家と呼びました。iframe iframe iframeブラウザによってメイン環境から分離された、独立したブラウザネイティブレベルの操作環境を作成できます。 iframeで実行されているスクリプト プログラムによってアクセスされるグローバル オブジェクトはすべて、現在のiframe実行コンテキストによって提供され、親ページの主な機能には影響しません。したがって、 iframeを使用してサンドボックスを実装することが、現時点では最も便利でシンプルかつ安全な方法です。

次のようなシナリオを想像してください。ページ内に複数のサンドボックス ウィンドウがあり、そのうちの 1 つはメイン ページといくつかのグローバル状態を共有する必要があり (例: ブラウザーの戻るボタンをクリックすると、サブアプリケーションも前のレベルに戻ります)、もう 1 つのサンドボックスはメイン ページといくつかの他のグローバル状態を共有する必要があります (例: Cookie のログイン状態を共有します)。

ブラウザはメインページとiframe間の通信にpostMessageやその他のメソッドを提供していますが、このシナリオを iframe のみを使用して実装するのは困難で、保守も不可能です。

3.5 ではサンドボックスを使用できるはずです (プロキシ + iframe 付き)

上記のシナリオを実現するには、上記の方法を組み合わせることができます。

  • iframeがグローバル オブジェクトから自然に分離されていることを利用して、 iframe.contentWindow現在のサンドボックスで実行されるグローバル オブジェクトとして取り出されます。
  • サンドボックスグローバルオブジェクトをwithのパラメータとして使用して内部実行プログラムのアクセスを制限し、 Proxyを使用してプログラム内部のアクセスを監視します。
  • 共有状態リストを維持し、外部と共有する必要があるグローバル状態をリストし、 Proxy内でアクセス制御を実装します。
//サンドボックスグローバルプロキシオブジェクトクラス class SandboxGlobalProxy {

    コンストラクター(共有状態) {
        // iframe オブジェクトを作成し、ネイティブ ブラウザのグローバル オブジェクトをサンドボックスのグローバル オブジェクトとして取り出します。const iframe = document.createElement('iframe', {url: 'about:blank'})
        document.body.appendChild(iframe)
        const sandboxGlobal = iframe.contentWindow // サンドボックスランタイムのグローバルオブジェクト return new Proxy(sandboxGlobal, {
            has: (target, prop) => { // has はコードブロック内の任意のプロパティへのアクセスを傍受できます if (sharedState.includes(prop)) { // プロパティが共有グローバル状態に存在する場合は、プロトタイプチェーンに沿って外側のレイヤーを検索します return false
                }

                ターゲットが独自のプロパティを持っている場合、
                    throw new Error(`無効な式 - ${prop}! これは実行できません!`)
                }
                真を返す
            }
        })

    }

}


関数 maybeAvailableSandbox(コード, ctx) {

    withedYourCode(code).call(ctx, ctx)

}

定数code_1 = `

    console.log(history == window.history) // false

    window.abc = 'サンドボックス'

    オブジェクト.prototype.toString = () => {

        console.log('トラップされました!')

    }

    console.log(window.abc) // サンドボックス

`

const sharedGlobal_1 = ['history'] // 外部実行環境と共有するグローバルオブジェクト const globalProxy_1 = new SandboxGlobalProxy(sharedGlobal_1)

おそらく利用可能なサンドボックス(code_1、globalProxy_1)


window.abc // 未定義

Object.prototype.toString() // [object Object] は印刷されません Traped

サンプルコードの結果から、 iframeの自然な環境分離の利点と、 with + Proxyの強力な制御を活用することで、サンドボックス内のグローバルオブジェクトと外部レイヤーのグローバルオブジェクトの分離を実現し、いくつかのグローバルプロパティの共有を実現していることがわかります。

3.6 サンドボックスエスケープ

サンドボックスは作成者にとってはセキュリティ戦略ですが、ユーザーにとっては制約となる可能性があります。創造的な開発者は、さまざまな方法でこの制約を取り除こうとしますが、これはサンドボックス エスケープとも呼ばれます。したがって、サンドボックス プログラムにとって最大の課題は、これらの予期しないプログラムの実行をどのように検出し、禁止するかということです。

上記で実装したサンドボックスは、私たちのニーズを満たしているようです。これで完了でしょうか?実際には、以下の操作はサンドボックス外の環境に影響を与え、サンドボックスからの脱出を実現します。

サンドボックス実行コンテキスト内のオブジェクトの内部プロパティにアクセスする場合、 Proxyこのプロパティのアクセス操作をキャプチャできません。たとえば、サンドボックスの実行コンテキストでwindow.parentを介して外部のグローバル オブジェクトを直接取得できます。

// サンドボックスオブジェクト内のオブジェクトのプロパティにアクセスする場合、上記のコードの一部は省略されます const ctx = {

    ウィンドウ: {

        親: {...}、

        ...

    }

}

定数コード = `

    ウィンドウ.親.abc = 'xxx'

`

ウィンドウ.abc // xxx

  • プロトタイプ チェーンにアクセスしてエスケープすることで、JS はリテラルを直接宣言し、リテラルのプロトタイプ チェーンを検索して外側のグローバル オブジェクトにアクセスできます。この動作も目に見えません。
定数コード = `

    ({}).コンストラクタ.プロトタイプ.toString = () => {

        console.log('脱出!')

    }

`

({}).toString() // エスケープ! [object Object] が必要です

3.7 「完璧な」サンドボックス(インタープリターのカスタマイズ)

上記の方法でサンドボックスを実装すると、多かれ少なかれ欠陥があります。完成に近いサンドボックスはありますか?

実際、多くのオープンソースライブラリは、ソースプログラム構造を分析して各ステートメントの実行ロジックを手動で制御するという、この手法をすでに行っています。このようにして、プログラムの実行時間を指定するコンテキストと、サンドボックスの制御を逃れようとする操作をキャプチャするコンテキストの両方が制御されます。このようなサンドボックスを実装することは、本質的にはカスタム インタープリターを実装することです。

関数almostPerfectSandbox(コード、ctx、違法操作) {

    return myInterpreter(code, ctx,illegalOperations) // カスタムインタープリター }

4. まとめ

この記事では、主にサンドボックスの基本的な概念とアプリケーション シナリオを紹介し、JavaScript サンドボックスの実装方法について考えていきます。サンドボックスの実装方法は静的なものではなく、その目標は特定のシナリオと組み合わせて分析する必要があります。さらに、サンドボックスからの脱出を防ぐことも、構築の初期段階ですべての実行caseをカバーすることが難しいため、長く困難な作業です。

Minecraft のように、サンドボックスは一晩で組み立てられるものではありません。

5. 参考

参考文献:

ソースコード: https://github.com/vuejs/vue/blob/v2.6.10/src/core/instance/proxy.js
コードサンボックス: https://codesandbox.io/
参照: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with
プロキシ: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
コードサンボックス: https://codesandbox.io/
JavaScript フレームワークの作成 - サンドボックス化されたコード評価: https://blog.risingstack.com/writing-a-javascript-framework-sandboxed-code-evaluation/
JS のサンドボックスについて: https://juejin.cn/post/6844903954074058760#heading-1

以下もご興味があるかもしれません:
  • Quickjs は JavaScript サンドボックスの詳細をカプセル化します
  • JavaScript サンドボックスの探索
  • フロントエンドJSサンドボックスを実装するいくつかの方法についての簡単な説明
  • Node.jsサンドボックス環境についての簡単な説明
  • Node.js アプリケーション用の安全なサンドボックス環境の設定
  • JS実装クロージャにおけるサンドボックスモードの例
  • JS サンドボックス モードの例の分析
  • JavaScript デザインパターン セキュリティ サンドボックス モード
  • WebWorkerはJavaScriptサンドボックスの詳細をカプセル化します

<<:  XHTMLはHTMLのいくつかの廃止された要素を使用しなくなりました

>>:  MySQL で最大接続数を正しく変更する 3 つの方法

推薦する

HTML で Web ページに動的な時計を書く

HTML を使用して動的な Web クロックを作成します。コードは次のとおりです。 <!DOC...

CSS3 で翻訳効果 (transfrom: translate) を実装する例

移動を実現するためにtranslateパラメータを使用しますtranslateX: X 軸に沿って移...

MySQL データテーブルのパーティション戦略と利点と欠点の分析

目次なぜパーティションが必要なのでしょうか?パーティショニング戦略パーティションの危険性なぜパーティ...

Dockerで作成したコンテナを削除する方法

Dockerで作成したコンテナを削除する方法1. まず、docker -s -aコマンドを使用してす...

MySql ビュー トリガー ストアド プロシージャの詳細な説明

ビュー:一時テーブルを繰り返し使用する場合、将来の使用を容易にするために別名を付けることができます。...

Mysql5.7.14 Linux版のパスワードを忘れた場合の完璧な解決策

/etc/my.confファイルで、[mysqld]の下に次の行を追加します: skip-grant...

JavaScript の矢印関数と通常の関数の違いの詳細な説明

この記事では、JavaScriptにおけるアロー関数と通常の関数の違いについて解説します。具体的な内...

Vueナンバープレート検索コンポーネントの使い方の詳しい説明

参考までに、シンプルなナンバープレート入力コンポーネント(vue)です。具体的な内容は次のとおりです...

Centos7 での mysql 8.0.15 のインストールと設定

この記事では、参考までにMySQL 8.0.15のインストールと設定のグラフィックチュートリアルを紹...

DockerコンテナでJupyterノートブックを設定する方法

Jupyter ノートブックは、主に Python コードの記述、より具体的にはディープラーニング開...

DockerコンテナでのMySQLデータのインポート/エクスポートの詳細な説明

序文MySQL データのインポートとエクスポートは mysqldump コマンドで解決できることは誰...

MySQLデータベースは重複データを削除し、メソッドインスタンスを1つだけ保持します

1. 問題の紹介ユーザー テーブルに 3 つのフィールドが含まれているシナリオを想定します。 id、...

フロントエンドパフォーマンス最適化に関する補足記事

序文私は、Web サイトのフロントエンド パフォーマンス最適化のための JavaScript と C...

WeChatアプレットが検索ボックス機能を実装

この記事の例では、WeChatアプレットの検索ボックス機能を実装するための具体的なコードを参考までに...