外部サイトを安全にプレビューする — プロキシ + サニタイズ + iframe sandbox の実装

ヒートマップエックス エンジニアチーム5分で読了
  • 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 付け忘れは、その典型例でした。

Claude Codeから動かすヒートマップを、まずは無料で。

計測タグを1行貼って、ブラウザ操作なしで分析・改善提案までCLIから受け取れます。クレカ不要・30秒でセットアップ。