Upgrade to Pro — share decks privately, control downloads, hide ads and more …

iframe sandboxでユーザー入力スクリプトを実行する

syumai
September 15, 2021

iframe sandboxでユーザー入力スクリプトを実行する

syumai

September 15, 2021
Tweet

More Decks by syumai

Other Decks in Programming

Transcript

  1. 本発表における 安全 の定義 アプリケーションの動作が破壊されないこと ユーザー入力スクリプトを実行することで、提供するアプリケーションのDOMが変 更されない アプリケーションの状態の変更が行われない => localStorage /

    sessionStorageなどの内容の書き換えが発生しない これが満たされないと、ユーザー入力スクリプトの内容によってアプリケーション全体をク ラッシュさせることが可能になる
  2. 実行したいスクリプトの例 加工前のObjectを保持する変数 const data = { a: 1, b: 2,

    c: 3 }; ユーザー入力スクリプト (Objectを加工する) { a: data.a * 2, c: data.c * 2 } 結果 (HTMLとして表示) key value a 2 c 6
  3. evalの使い方 式、または文 (および複数の文) を渡すと、それを評価して結果を返す const a = 2; const b

    = 3; eval("a * b"); // => 6 ( 式の評価) eval("a * b; a + b"); // => 5 ( 文の評価)
  4. evalの特徴 スコープを引き継ぐ evalが呼ばれた箇所のローカルスコープを引き継ぐ 危険: 本来使って欲しくない変数まで使えてしまう 低速: eval中で使われる変数名などをバイトコード中から探すため // 与えられたObject を

    `query` で変形する関数 function transformObject(obj, query) { const __SECRET_FUNCTION = () => console.log("kaboom! "); return eval(`(${query})`); } const data = { a: 2, b: 3 }; console.log( transformObject(data, "{ a: obj.a * 2, b: obj.b * 2 }") // => `{ a: 4, b: 6 }` ); console.log( transformObject(data, "__SECRET_FUNCTION()") // => `kaboom! ` );
  5. Function() の使い方 新たな関数を生成する 引数名の一覧と、関数のbodyを渡して使う const a = 2; const b

    = 3; const sum = new Function("x", "y", "return x + y;"); sum(a, b); // => 5 // `new` を書かなくても同様に使える const sum2 = Function("x", "y", "return x + y;");
  6. Function() の特徴 関数の生成に伴って、新たなスコープが切られる 本来使って欲しくない変数が使われることがない // 与えられたObject を `query` で変形する関数 function

    transformObject(obj, query) { const __SECRET_FUNCTION = () => console.log("kaboom! "); const transform = new Function("obj", `return (${query});`); return transform(obj); } const data = { a: 2, b: 3 }; console.log( transformObject(data, "{ a: obj.a * 2, b: obj.b * 2 }") // => `{ a: 4, b: 6 }` ); console.log( transformObject(data, "__SECRET_FUNCTION()") // => `Uncaught ReferenceError: __SECRET_FUNCTION is not defined` );
  7. ユーザー入力スクリプトへのstrict modeの適用 strict modeの適用も出来る // 与えられたObject を `query` で変形する関数 function

    transformObject(obj, query) { const __SECRET_FUNCTION = () => console.log("kaboom! "); const transform = new Function("obj", `"use strict"; return (${query});`); return transform(obj); } const data = { a: 2, b: 3 }; console.log( transformObject(data, "with({ x: 100 }) { x }") // => `Uncaught SyntaxError: Unexpected token 'with'` );
  8. Function() は 安全 の定義を満たすか アプリケーションの動作が破壊されないこと ユーザー入力スクリプトを実行することで、提供するアプリケーションのDOMが変 更されない => 変更できる window

    Objectを参照可能なので、自由にDOMを追加したり、操作した りできる アプリケーションの状態の変更が行われない => 変更できる アプリケーションと同じページ上で動作するので、localStorage / sessionStorageなどを自由に書き換えできる => 満たしていない
  9. Originとは eijiさんの記事より Origin は scheme (http とか https とか)、hostname、port の組み合わせを指す。 same-origin

    と言った場合、これらすべてが一致するものを示している same-site/cross-site, same-origin/cross-originをちゃんと理解する: https://zenn.dev/agektmr/articles/f8dcd345a88c97
  10. Window Objectの参照とは? iframe, window.openなどで得られる 参照取得方法の例 経路 親 => 子 子

    => 親 iframe iframe.contentWindow window.parent window.open(url) open関数の返り値 window.opener <a ... target="_blank"> - window.opener
  11. Window Objectを経由したプロパティアクセス 同一Originの例 https://a.com/index.html に https://a.com/iframe.html を埋め込む https://a.com/index.html のHTML <iframe

    src="https://a.com/iframe.html"> </iframe> https://a.com/iframe.html のHTML <script> window.parent.document.write("kaboom! "); </script> => https://a.com/index.html を表示すると、 kaboom! と画面に表示される
  12. Window Objectを経由したプロパティアクセス 別Originの例 https://a.com/index.html に https://b.com/iframe.html を埋め込む https://a.com/index.html のHTML <iframe

    src="https://b.com/iframe.html"> </iframe> https://b.com/iframe.html のHTML <script> window.parent.document.write("kaboom! "); </script> => 例外が発生する。内容: DOMException: Blocked a frame with origin "https://b.com" from accessing a cross-origin frame.
  13. iframe sandboxを使う! iframeにsandbox属性を設定すると、iframeの機能を制限できる sandbox属性を使うと、iframe内は特殊なOrigin ( null ) として、常にSame-origin policyに失敗させられる 同一Originからiframeのコンテンツを配信しても安全に出来る

    <iframe src="xxx" sandbox></iframe> <!-- 全て不許可 --> <iframe src="xxx" sandbox="allow-scripts"></iframe> <!-- JavaScript の実行のみ許可 --> sandbox属性はホワイトリスト形式 デフォルトは全て不許可 実は、 allow-popups なども存在する 入力スクリプトによる window.alert などの実行をセットで防げる
  14. 実装 execScript関数を作る その中でiframeを生成する形にする function execScript(script) { const iframe = document.createElement("iframe");

    iframe.sandbox = "allow-scripts"; iframe.contentWindow // 別Origin のWindow Object が入手できた! ... }
  15. WindowのpostMessageを使う! Cross originではWindow Objectのプロパティアクセスが基本的に不可能 postMessage(message, targetOrigin) を使ったmessageの送受信は可能 iframeの例 親Window (https://a.com)

    const iframe = ... // https://b.com を埋め込んだiframe を取得する iframe.contentWindow.postMessage({ message: "hello!" }, "https://b.com"); 子Window (https://b.com) window.addEventListener("message", (event) => { console.log(event.data); // `{ message: "hello!" }` })
  16. postMessageの注意点 受信側が、どこからmessageを受け取るか選べない message イベントハンドラが複数Window Objectからの受信に対して共通になる origin / sourceのチェックは必須 window.addEventListener("message", (event)

    => { // https://a.com から受け付けたmessage しか処理しない! if (event.origin !== "https://a.com") { return; } // iframe の親Window から受け付けたmessage しか処理しない! if (event.source !== window.parent) { return; } ... });
  17. 実装 function execScript(script) { ... iframe.srcdoc = ` <script> window.addEventListener("message",

    (event) => { if (event.origin !== "${window.location.origin}") { return; } if (event.source !== window.parent) { return; } // 受け取ったscript の実行 const fn = new Function(event.data.script); // 結果を返す window.parent.postMessage({ result: fn() }, "${window.location.origin}") }); </script> `; // ユーザー入力スクリプトをiframe に送信する // `*` を指定するのは、`null` origin にmessage を送るため iframe.postMessage({ script }, "*"); ... } ※ ) 本当はiframeがmessageを受信出来る状態になるまで待つ必要がありますが、ここでは省略します
  18. 実装 2 で結果を返すところまで実装したので、あとはそれを受け取るだけ function execScript(script) { ... window.addEventListener("message", (event) =>

    { // `null` origin からの受信になる。ここのチェックは一旦skip // if (event.origin !== "null") { // return; // } if (event.source !== iframe.contentWindow) { return; } console.log(event.data); // 結果が入手できた! }); }
  19. Promiseを使う どうしても非同期になってしまうので、Promiseで結果を取得できるようにする function execScript(script) { ... return new Promise(resolve =>

    { window.addEventListener("message", (event) => { if (event.source !== iframe.contentWindow) { return; } resolve(event.data); }); } }
  20. 引数を指定できるようにする // args Object を受け付ける function execScript(script, args) { ...

    iframe.srcdoc = ` <script> window.addEventListener("message", (event) => { ... const { script, args } = event.data; // args Object をkey とvalue の配列に分解する const keys = Object.keys(args); const values = Object.values(args); // args をFunction constructor に渡す const fn = new Function(...keys, script)(...values); window.parent.postMessage({ result: fn() }, "${window.location.origin}") }); </script> `; iframe.postMessage({ script, args }, "*"); ... }
  21. 完成! const a = 2; const b = 3; await

    execScript("a * b", { a, b }); // 6
  22. postMessageの機能についての補足 もし、postMessageを使って関数の送信が可能であれば、悪意を持った関数を親 Windowに送って実行させることが出来ると考えられる これは可能か? => 不可能! postMessageによって送られるObjectは 構造化複製アルゴリズム によって複製さ れる

    WebWorker / IndexedDBとの値のやり取り等に使われるアルゴリズムと同じ このアルゴリズムでは、 関数は複製不可能なので送信時にエラーとなる。 詳細はこちら: https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Structured_clo ne_algorithm
  23. sandbox化されたiframeで実行出来るスクリプトについての補足 iframeのメッセージ受信機構を破壊できるか? => 出来ます。 window.locationが書き込み可能なので、iframeに表示中のページを遷移するこ とが出来ます。 もし、遷移先に message handlerが設定されていた場合、親Windowから送信 したメッセージを対象のWindowから読まれる可能性がある点に注意です。

    今回のユースケースでは、スクリプトの実行者はスクリプトを作成した本 人となるため許容出来ます。 メッセージ受信機構が破壊されても機能を完全に停止させないようにするため には、(今回の実装のように、)iframeを関数呼び出しの度に生成するのがよい のではないかと考えています。