Rebounder Tech Blog

運用している当事者が書く、本番システムの記録。

CSPのframe-src欠落でYouTube埋め込みが全滅、視聴ログも無音で止まる

公開 読了時間 約6分執筆: Rebounder 開発チーム(当該システムの運用当事者)

※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。

結論

CSPにframe-srcが無いと、script-srcにYouTubeのドメインを許可していてもdefault-src 'self'が効いて埋め込み動画は1本も再生できず、IFrame APIが依存するwindow.YTも生えないため視聴ログは静かに止まる。

結論

CSPにframe-srcが無い状態では、script-srcにYouTubeのドメインを許可していてもdefault-src 'self'が効いてiframe埋め込みそのものがブロックされ、YouTube動画は1本も再生できない。 さらにscript-srcにもYouTubeのドメインが無ければIFrame Player APIのスクリプト自体が読み込めず、プレイヤーが依存するwindow.YTが定義されないため、表示が止まるだけでなく再生状態を監視して送る視聴ログの記録も同時に止まる。直し方はscript-srcimg-srcframe-srcの3ディレクティブへ、YouTubeが実際に使うドメインだけを個別に追加することだった。

症状

学習動画教材のCSPはnext.config.tssecurityHeadersで1箇所に定義されており、修正前は次のようになっていた。

const securityHeaders = [
  // ...
  {
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
      "style-src 'self' 'unsafe-inline'",
      `connect-src 'self' https://*.supabase.co wss://*.supabase.co`,
      "img-src 'self' data:",
      "font-src 'self'",
      "frame-ancestors 'none'",
    ].join('; '),
  },
]

frame-srcディレクティブがそもそも存在しない。CSPは、個別のディレクティブが指定されていない場合default-srcにフォールバックする仕様のため、frame-src未指定の状態はframe-src 'self'と同義になる。結果として、<iframe>でYouTubeを埋め込んでいる箇所は本番で例外なくブロックされ、2026-08-16に本番で確認された。

原因

このCSPは学習動画の埋め込みを最初から想定して書かれたものではなく、YouTube用のドメインが一つも列挙されていなかった。影響は表示だけに留まらない。VideoPlayerコンポーネントはYouTube IFrame Player APIに依存しており、window.YTが使えるようになるのを待ってから初めてプレイヤーを生成する作りになっている。

if (window.YT?.Player) {
  attachPlayer()
} else {
  const prev = window.onYouTubeIframeAPIReady
  window.onYouTubeIframeAPIReady = () => { prev?.(); attachPlayer() }
  if (!document.getElementById('yt-iframe-api')) {
    const s  = document.createElement('script')
    s.id     = 'yt-iframe-api'
    s.src    = 'https://www.youtube.com/iframe_api'
    document.head.appendChild(s)
  }
}

https://www.youtube.com/iframe_apiを読み込むスクリプトタグを動的に挿入しているが、修正前のscript-src'self' 'unsafe-inline' 'unsafe-eval'だけで、外部ドメインが一切許可されていない。このためiframe_apiスクリプト自体がCSP違反でブロックされ、window.YTが定義されないままになる。VideoPlayerwindow.YTが生えて初めてプレイヤーを組み立て、視聴時間の計測とハートビート送信を開始する作りのため、frame-srcを直すだけでは動画は表示されても視聴の記録側はまだ動かない。両方のディレクティブが揃って初めて、埋め込みの表示とAPI経由の視聴計測の両方が成立する。

直し方

script-srcimg-srcframe-srcの3箇所に、YouTubeが実際に使うドメインだけを個別に追加した。

"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.youtube.com https://s.ytimg.com",
"style-src 'self' 'unsafe-inline'",
`connect-src 'self' https://*.supabase.co wss://*.supabase.co${devConnectSrc}`,
"img-src 'self' data: https://i.ytimg.com",
"font-src 'self'",
"frame-src https://www.youtube.com https://www.youtube-nocookie.com",
"frame-ancestors 'none'",

内訳は次の通り。

  • script-srchttps://www.youtube.com(IFrame API本体の配信元)とhttps://s.ytimg.com(API内部が追加で読み込むスクリプト)
  • img-srchttps://i.ytimg.com(サムネイル画像の配信元)
  • frame-srchttps://www.youtube.comhttps://www.youtube-nocookie.com(埋め込み本体)

いずれもYouTube関連のドメインに限定しており、unsafe-inlineのようなワイルドカードの緩和は加えていない。あわせて、開発時だけconnect-srcにlocalhostへの接続を許可する分岐も追加したが、これはNODE_ENV === 'production'で無効化されるため本番のCSPには影響しない。

const devConnectSrc =
  process.env.NODE_ENV === 'production' ? '' : ' http://127.0.0.1:* http://localhost:* ws://127.0.0.1:* ws://localhost:*'

再発防止

このCSPはnext.config.tsの1箇所に集約されており、追加するディレクティブ自体は素直だった。見つけるのが遅れた理由は、CSP違反がビルドエラーにもTypeScriptの型エラーにもならず、実際にブラウザで動画を再生してコンソールを見るまで気づけない性質の不具合だったためだ。埋め込みが失敗しても<iframe>要素自体はDOM上に存在し続けるため、画面を一目見ただけでは「動画が表示されない」ことと「CSPでブロックされている」ことの区別がつきにくい。修正の確認も同じ理由で、コンソールのCSPエラーが0件であること・window.YTobjectになっていること・実際にプレイヤーとサムネイルが表示されることの3点を、実機のブラウザで見て初めて確定させている。

よくある質問

Q1frame-srcが無いとYouTube埋め込みはどうなりますか?

default-src 'self'にフォールバックし、iframeでのYouTube埋め込みが全てブロックされます。ブラウザのコンソールには「Framing 'https://www.youtube.com/' violates ... default-src 'self'」というCSPエラーが出て、動画は再生されません。

Q2script-srcにYouTubeを許可しないとどうなりますか?

IFrame Player APIのスクリプト(https://www.youtube.com/iframe_api)自体が読み込めず、window.YTが定義されないままになります。frame-srcだけ直しても、window.YTに依存する再生状態の監視や視聴時間の記録は動きません。

Q3この修正はローカル開発環境にどう影響しますか?

本番のCSPは変えず、開発時(NODE_ENV !== 'production')だけconnect-srcにlocalhostへの接続を追加しました。ローカルでSupabaseを起動して確認する用途向けで、本番のポリシーはYouTube関連ドメインの追加以外そのままです。

確認した環境

  • Next.js 16.2.10 / React 19.2.4
  • 2026-08-16 に本番で確認・同日修正

この記事の根拠

  • TypeScriptファイル 1〜21行目コミット ef94f1b
  • TypeScriptファイル 1〜33行目コミット e1801b6
  • TypeScriptファイル 145〜156行目コミット e1801b6

本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。