外部サイトを安全にプレビューする — プロキシ + サニタイズ + iframe sandbox の実装
- engineering
- security
- nextjs
- iframe
この記事のまとめ
- 外部サイトを自社オリジンのiframeに読み込むと、外部JSが自社セッションを触れてしまう危険がある
- 対策は「SSRFガード付きプロキシ + 外部JSの除去 + 制限CSP + iframe sandbox」の多層防御
- sandbox は強力すぎると自前スクリプトまで止める。
allow-scriptsの付け忘れで機能が動かない落とし穴もある
HeatMapX のA/Bテストには、「変更後(Variant B)の見た目を、対象ページに適用した状態でプレビューする」機能があります。これは技術的には「ユーザーの外部サイトを、自社ドメインのiframe内で安全に表示する」という、意外とデリケートな処理です。設計と、実際にハマった落とし穴を共有します。
なぜ素朴な実装が危険か
外部サイトのHTMLをそのまま取得して自社オリジン(例:heatmapx.com)で配信すると、そのHTMLに含まれる <script> や onclick= などが 自社オリジンの権限で実行 されてしまいます。すると、Cookie やログインセッションを読み取られる恐れがあります。これは避けなければなりません。
多層防御の構成
そこで、次の層を重ねています。
1. SSRFガード付きプロキシ
取得先URLを検証し、http/https 以外を拒否。さらに localhost・プライベートIP・クラウドのメタデータエンドポイント(169.254.169.254 等)を弾きます。リダイレクトも自動追従せず、redirect: 'manual' で1段ずつ再検証し、内部ネットワークへ誘導されないようにします。
2. 外部JSの除去(サニタイズ)
取得したHTMLから <script> タグと on*= インラインハンドラ、javascript: スキームを除去します。プレビューに顧客サイトのJSを動かす必要はないためです(SPAが完全再現できないのは既知の割り切り)。
3. 制限CSPと base href
<head> 直後に <base href> を挿入して相対パスの画像・CSSを解決しつつ、レスポンスには「外部JSは動かさない」制限CSPを付与します。画像・CSSは表示のため許可しています。
4. iframe sandbox
最後に、表示側のiframeに sandbox を付けて隔離します。
ハマった落とし穴:sandbox が強すぎた
プレビューは、取得HTMLに「変更(changeSet)を適用する小さな自前スクリプト」を注入し、iframe内で実行して見た目を変えます。ところが、プレビュー用iframeの sandbox が allow-same-origin のみで、allow-scripts が付いていませんでした。
結果、自前スクリプトも実行されず、変更が当たらないまま元ページが表示されるという不具合に。編集用のエディタ側iframeは allow-scripts allow-same-origin で正しく動いていたため、設定の食い違いが原因でした。
修正はシンプルで、プレビュー側も allow-scripts allow-same-origin に揃えるだけ。あわせて、iframeの sandbox 属性を検証する回帰テストを追加しました。
sandbox="allow-same-origin" → sandbox="allow-scripts allow-same-origin"
学び
- 外部コンテンツの埋め込みは「多層防御」で考える。1枚の対策に頼らない。
sandboxは「付ければ安全」ではなく、必要な権限だけを正確に許可するのが肝心。強すぎると自分の機能まで止まる。- 「同じ処理をする2つの経路」(エディタとプレビュー)は、セキュリティ属性を揃えたか必ず確認する。片方だけ直すと片方が壊れる。
まとめ
外部サイトの安全なプレビューは、SSRFガード・外部JS除去・制限CSP・iframe sandbox の多層防御で実現できます。そして sandbox の権限は、足りなくても多すぎても問題になります。今回の allow-scripts 付け忘れは、その典型例でした。