Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
iframe sandboxでユーザー入力スクリプトを実行する
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
syumai
September 15, 2021
Programming
14k
14
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
iframe sandboxでユーザー入力スクリプトを実行する
syumai
September 15, 2021
More Decks by syumai
See All by syumai
作って学ぶ、 JSX (TSX) ランタイムの基本
syumai
7
1.6k
Oxlintのカスタムルールの現況
syumai
6
1.1k
Oxlintはいかにしてtsgolintのlint ruleを呼び出しているのか
syumai
2
1.2k
『[入門] Cloudflare Workers』本はなぜ誕生したのか
syumai
0
390
tsgolintはいかにしてtypescript-goの非公開APIを呼び出しているのか
syumai
9
3.1k
知られているようで知られていない JavaScriptの仕様 4選
syumai
3
1.2k
CloudflareのSandbox SDKを試してみた
syumai
0
870
実践AIチャットボットUI実装入門
syumai
9
4.2k
ProxyによるWindow間RPC機構の構築
syumai
3
1.5k
Other Decks in Programming
See All in Programming
「エンジニアインターン、どうやって取った?」準備のリアルを語るLT会 Progate BAR
akiomatic
0
140
AIとASP.NET Coreで雑Webアプリを作った話
mayuki
0
660
Agentic UI
manfredsteyer
PRO
0
180
Vite+ Unified Toolchain for the Web
naokihaba
0
320
Contextとはなにか
chiroruxx
1
330
LLMによるContent Moderationの本番運用の裏側と品質担保への挑戦
suikabar
3
710
Honoでのサプライチェーン侵害対策 〜 3つのライブラリに学ぶ
yusukebe
6
1.3k
スマートグラスで並列バイブコーディング
hyshu
0
160
AIだと陥りがちなJakarta EE最新技術への移行時の落とし穴と解決策
tnagao7
0
110
生成AI時代にこそ効くGo | Why Go Works in the Age of Generative AI
mom0tomo
8
3.3k
[2026年度第1回ORセミナー] 計画最適化ベンチャーと競技プログラミング人材
terryu16
0
270
DynamoDBには集計系のクエリがないけどなんとかしたい
musan
1
180
Featured
See All Featured
Amusing Abliteration
ianozsvald
1
210
Making the Leap to Tech Lead
cromwellryan
135
9.9k
Speed Design
sergeychernyshev
33
1.9k
Put a Button on it: Removing Barriers to Going Fast.
kastner
60
4.3k
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
333
22k
Side Projects
sachag
455
43k
Agile Actions for Facilitating Distributed Teams - ADO2019
mkilby
0
210
What's in a price? How to price your products and services
michaelherold
247
13k
Breaking role norms: Why Content Design is so much more than writing copy - Taylor Woolridge
uxyall
0
320
Mozcon NYC 2025: Stop Losing SEO Traffic
samtorres
1
260
DBのスキルで生き残る技術 - AI時代におけるテーブル設計の勘所
soudai
PRO
66
55k
Everyday Curiosity
cassininazir
0
230
Transcript
iframe sandboxでユーザー入力スクリプトを実行する syumai フロントエンドLT会 - vol.4 (2021/9/15)
自己紹介 syumai 普段はTypeScript (React / Next.js) やGoを書きつつ生活しています Twitter: @__syumai Website:
https://syum.ai
本日のテーマ
iframe sandboxでユーザー入力スクリプトを実行する
元々何がしたかったのか?
ユーザーが入力したJavaScriptを安全に実行したかった
前提 提供するアプリケーション上で、ユーザーが、自身の書いたスクリプトを自身のページ 内で実行することを出来るようにしたい 不特定多数の人がスクリプトを実行するような使い方はしない
本発表における 安全 の定義とは?
本発表における 安全 の定義 アプリケーションの動作が破壊されないこと ユーザー入力スクリプトを実行することで、提供するアプリケーションのDOMが変 更されない アプリケーションの状態の変更が行われない => localStorage /
sessionStorageなどの内容の書き換えが発生しない これが満たされないと、ユーザー入力スクリプトの内容によってアプリケーション全体をク ラッシュさせることが可能になる
実行したいスクリプトの例
実行したいスクリプトの例 加工前の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
ユーザー入力スクリプト←ここをどう実行するかを考える { a: data.a, c: data.c * 2 }
その上で、下記のような入力がアプリケーションを破壊しないようにする document.write("kaboom! "); // 画面上に `kaboom! ` だけが表示される window.localStorage.clear(); //
localStorage 内の全データを消す
調べたこと 1. ユーザー入力スクリプトを実行する方法 2. ユーザー入力スクリプトの実行を安全に行う方法
1. ユーザー入力スクリプトを実行する方法
方法は2つ 1. eval 2. Function() constructor
eval
evalの使い方 式、または文 (および複数の文) を渡すと、それを評価して結果を返す const a = 2; const b
= 3; eval("a * b"); // => 6 ( 式の評価) eval("a * b; a + b"); // => 5 ( 文の評価)
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! ` );
MDNより https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/eval
Function() constructor
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;");
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` );
ユーザー入力スクリプトへの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'` );
(本発表の定義において) Function() は安全か?
Function() は 安全 の定義を満たすか アプリケーションの動作が破壊されないこと ユーザー入力スクリプトを実行することで、提供するアプリケーションのDOMが変 更されない => 変更できる window
Objectを参照可能なので、自由にDOMを追加したり、操作した りできる アプリケーションの状態の変更が行われない => 変更できる アプリケーションと同じページ上で動作するので、localStorage / sessionStorageなどを自由に書き換えできる => 満たしていない
2. ユーザー入力スクリプトの実行を安全に行う方法
どうすれば安全にできるか?
どうすれば安全にできるか? window Objectを参照可能なので、自由にDOMを追加したり、操作したりできる window Objectの危険なプロパティを参照できないようにすれば、DOMを操作で きなくなる アプリケーションと同じページ上で動作するので、localStorage / sessionStorageなど を自由に書き換えできる
localStorage / sessionStorageなどを参照出来ない場所でscriptを実行すれば書き 換えられない
これらを満たすには? => 別Originでscriptを実行すればOK!
Same-origin policy
Same-origin policyとは MDNより あるオリジンによって読み込まれた文書やスクリプトが、他のオリジンにあるリソース にアクセスできる方法を制限するものです。 https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy
Originとは eijiさんの記事より Origin は scheme (http とか https とか)、hostname、port の組み合わせを指す。 same-origin
と言った場合、これらすべてが一致するものを示している same-site/cross-site, same-origin/cross-originをちゃんと理解する: https://zenn.dev/agektmr/articles/f8dcd345a88c97
Same-origin policyによってアクセスが制限されるもの iframe、window.openなどによって得たwindow Objectのプロパティ (一部除く) Access-Control-Allow-Originヘッダが設定されていないリソースへのfetch / XHR など
Same-origin policyによってアクセスが制限されるもの iframe、window.openなどによって得たWindow Objectのプロパティ (一部除く) 今回関心があるのはこちら
Window Objectの参照とは? iframe, window.openなどで得られる 参照取得方法の例 経路 親 => 子 子
=> 親 iframe iframe.contentWindow window.parent window.open(url) open関数の返り値 window.opener <a ... target="_blank"> - window.opener
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! と画面に表示される
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.
Cross originでのWindow Objectの挙動 ごく一部を除いて、基本全てのプロパティへのアクセスが禁じられる ユーザー入力スクリプトを実行することで、提供するアプリケーションのDOMが変 更されない と言う条件を満たすのに使えそう! localStorage / sessionStorageへのアクセスも禁じられる
アプリケーションの状態の変更が行われない と言う条件を満たすのに使えそう!
Same-origin policyを踏まえた実装方針 別OriginのWindow Objectを入手して、その中でscriptを実行すること に決定
実装
やること 1. 別OriginのWindow Objectを入手する 2. 1 のWindowにユーザー入力スクリプトを渡して実行する 3. 実行結果を入手する
1. 別OriginのWindow Objectを入手する
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 などの実行をセットで防げる
危険! allow-same-origin allow-same-origin を指定すると、通常の Same-origin policy が適用され、同一 Originだった場合 iframe内から親WindowのDOM操作などが可能になる そもそもsandbox属性の内容も書き換えられるし、何でも出来ちゃう
今回の用途では絶対にNG <iframe src="xxx" sandbox="allow-scripts allow-same-origin"></iframe> <!-- 要注意! -->
実装 execScript関数を作る その中でiframeを生成する形にする function execScript(script) { const iframe = document.createElement("iframe");
iframe.sandbox = "allow-scripts"; iframe.contentWindow // 別Origin のWindow Object が入手できた! ... }
2. 1 のWindowにユーザー入力スクリプトを渡して実行 する
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!" }` })
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; } ... });
実装 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を受信出来る状態になるまで待つ必要がありますが、ここでは省略します
3. 実行結果を入手する
実装 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); // 結果が入手できた! }); }
実装の工夫
Promiseを使う どうしても非同期になってしまうので、Promiseで結果を取得できるようにする function execScript(script) { ... return new Promise(resolve =>
{ window.addEventListener("message", (event) => { if (event.source !== iframe.contentWindow) { return; } resolve(event.data); }); } }
引数を指定できるようにする // 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 }, "*"); ... }
完成! const a = 2; const b = 3; await
execScript("a * b", { a, b }); // 6
デモはこちら https://github.com/syumai/sandboxed-eval
まとめ ユーザー入力スクリプトの安全な実行には別OriginのWindowが使える 別OriginのWindowは、iframe sandboxで簡単に使える Window Object間のmessage 送受信にはpostMessageが使える
終
補足
postMessageの機能についての補足 もし、postMessageを使って関数の送信が可能であれば、悪意を持った関数を親 Windowに送って実行させることが出来ると考えられる これは可能か? => 不可能! postMessageによって送られるObjectは 構造化複製アルゴリズム によって複製さ れる
WebWorker / IndexedDBとの値のやり取り等に使われるアルゴリズムと同じ このアルゴリズムでは、 関数は複製不可能なので送信時にエラーとなる。 詳細はこちら: https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Structured_clo ne_algorithm
sandbox化されたiframeで実行出来るスクリプトについての補足 fetch / XHRは実行出来るか? => 出来ます。 Originが null になるだけで、外部にリクエストは普通に送ることが出来る。 今回のようなユースケースでなければ、場合によっては都合が悪い可能性があ
る トラフィックの多いページにこのスクリプト実行機構を設置すると、意図せず 外部サイトへの大量リクエストが飛ぶ可能性があるので要注意
sandbox化されたiframeで実行出来るスクリプトについての補足 iframeのメッセージ受信機構を破壊できるか? => 出来ます。 window.locationが書き込み可能なので、iframeに表示中のページを遷移するこ とが出来ます。 もし、遷移先に message handlerが設定されていた場合、親Windowから送信 したメッセージを対象のWindowから読まれる可能性がある点に注意です。
今回のユースケースでは、スクリプトの実行者はスクリプトを作成した本 人となるため許容出来ます。 メッセージ受信機構が破壊されても機能を完全に停止させないようにするため には、(今回の実装のように、)iframeを関数呼び出しの度に生成するのがよい のではないかと考えています。