マガジン

CSPとは何を禁じているか:ゲームファイルだけに厳しい理由

遊びを見つける

執筆にClaudeを使用しています。

同じ絵の厚いカード束と薄い束を比べ、画像容量の最適化を表した写真風イメージ。
記事のテーマをもとに制作したAI生成イラスト。

ゲームを作る人が、外部の配信サービスからライブラリを読み込む1行を足す場面を考えてみます。手元ではきちんと動いたのに、公開後に開くと画面が真っ白のままです。コンソールを開くと、「Content Security Policy の指示に違反するため読み込みを拒否しました」という趣旨の文が出ています。コードのどこにも誤りはなく、止めているのはサーバーが返した応答ヘッダーの一行です。 この一行が CSP(Content Security Policy、コンテンツ・セキュリティ・ポリシー)です。ページが「どこから何を読み込んでよいか」「どこへ通信してよいか」をブラウザに宣言し、宣言から外れた読み込みや通信をブラウザ自身に止めさせる仕組みです。この記事では、CSPが何を禁じるのかを整理し、手元で挙動を確かめる最小の実験を示します。そのうえで、アソバースのように他人の作ったゲームを預かる場所で、ゲームのファイルだけが特別に扱われる理由を、確認済みの事項から読み取れる範囲で考えます。 先に断っておくと、アソバースの案内で確認済みなのは「sandbox付きiframeで実行する」「アップロードしたコードをメインサイトと別のオリジンから配信する」ことまでです。CSPヘッダーの具体的な中身は、確認済みの事項に含まれていません。そのため中身は推測せず、自分で確かめる手順を後半に示します。

この記事の目次
  1. 一行のヘッダーが止めるもの
  2. 手元で確かめる最小の実験設計
  3. 「ゲームのファイルだけ」が別扱いになる理由
  4. アソバースの確認済み事項から読み取れること
  5. 本物のヘッダーを自分で確かめる手順
  6. 次の行動
  7. 参考資料

一行のヘッダーが止めるもの

CSPは、Content-Security-Policy というHTTP応答ヘッダー(またはHTMLの meta タグ)に、項目ごとの許可リストを書く形です。代表的な項目を並べます。

  • default-src、制限の対象:個別に指定しなかった読み込み全般の既定
  • script-src、制限の対象:JavaScriptの読み込みと実行(ページ内に直接書いたスクリプトを含む)
  • connect-src、制限の対象:fetch、XMLHttpRequest、WebSocketなどの通信先
  • img-src、制限の対象:画像の読み込み元
  • frame-src、制限の対象:iframeに入れてよいページ

ここで大事なのは、禁じる対象が「悪いコード」ではなく「宛先と種類」だという点です。ブラウザはコードの意図を読めません。そこで、許可していない宛先への通信や読み込みを一律に止めます。

たとえば connect-src 'none' と宣言されたページでは、ゲームが何を送ろうとしても、通信そのものが成立しません。作者が悪意を持っていなくても、うっかり入れた計測用スクリプトが外部へ情報を送ることまで止まります。

手元で確かめる最小の実験設計

CSPは、短いHTMLファイル1枚で挙動を確かめられます。問いは「通信と画像の読み込みを許可しないと宣言したページで、ブラウザは何を報告するか」です。準備するものは、テキストエディタと最新のブラウザだけです。次の内容を csp-test.html として保存し、ブラウザで開いてください。

html
<!doctype html>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy"
  content="default-src 'none'; script-src 'unsafe-inline'; connect-src 'none'; img-src data:">
<pre id="log">開始\n</pre>
<script>
const log = t => document.getElementById('log').textContent += t + '\n';
document.addEventListener('securitypolicyviolation', e =>
  log('ブラウザが報告: ' + e.violatedDirective + ' / ' + e.blockedURI));
fetch('https://example.com/').then(
  () => log('通信の結果: 成功'),
  () => log('通信の結果: 失敗'));
const img = new Image();
img.onerror = () => log('画像の結果: 読み込めず');
img.src = 'https://example.com/a.png';
</script>

記録は次のように取ります。

  1. 画面に出た行を、上から順にそのまま書き写します(1回で十分です)。
  2. 開発者ツールのコンソールとネットワークの表示も見て、ブロックの文言が出ているか確認します。
  3. meta タグの connect-src 'none' と img-src data: を削除して、もう一度開きます。
  4. 手順1の記録と並べて、行がどう変わったかを比べます。

結果の読み方は次のとおりです。ポリシーあり側で「ブラウザが報告」の行が現れ、connect-src や img-src といった項目名が見えれば、禁止が宣言どおりに働いています。ポリシーを外した側でその行が消えれば、止めていたのは確かにその一行です。

起こりやすい誤りは3つあります。

  • meta タグを、止めたいスクリプトより後ろに書いてしまう。
  • 通信失敗の原因を、ネットワークの不調と取り違える。
  • script-src を書かずに自分のスクリプトまで止めてしまい、何も表示されなくなる。

なお、'unsafe-inline' はページ内に直接書いたスクリプトを許可する指定です。この実験のためだけに入れており、実際の公開ページで安易に使う指定ではありません。

「ゲームのファイルだけ」が別扱いになる理由

ゲームはHTMLとJavaScriptの塊です。つまり、プラットフォームから見れば「他人の書いたコードを、閲覧者の端末で実行する」ことになります。ここで同じオリジン(スキーム、ホスト、ポートの組)でサイト本体と同居させると、そのコードはサイト本体と同じ権限で動けてしまいます。

具体的には、サイトのlocalStorageを読んだり、ログイン状態を使ってサイト本体の機能を呼び出したりする経路が開く可能性があります。ブラウザの同一オリジンポリシーは、オリジンが同じなら自由に触れてよいという前提で作られているからです。

そこで、預かったコードは次の三重の囲いに置くのが一般的な設計です。

  • 別のオリジンから配信する。サイト本体とは「別の家」になるため、互いのデータに触れられません。
  • iframeに sandbox を付ける。ポップアップやフォーム送信などの許可を、項目ごとに絞れます。
  • CSPで通信と読み込みの宛先を絞る。囲いの内側から外へ出る道を、さらに狭められます。

この三つは役割が違います。別オリジンは「何に触れられるか」を、sandboxは「ブラウザの機能のうち何を使えるか」を、CSPは「どこへつながれるか」を受け持ちます。「ゲームのファイルだけに厳しい」とは、サイト本体の画面には通常どおりの機能を許し、他人のコードが走る領域だけをこの三重の囲いで包むという意味で読めます。

アソバースの確認済み事項から読み取れること

ここからは「設計から読み取れること」として整理します。根拠にするのは、次の確認済みの事項だけです。

  • ゲームはsandbox付きiframeで実行される。
  • アップロードしたコードは、メインサイトと別のオリジンから配信される。
  • サーバー用コード、サービスワーカー、SharedArrayBufferを使うマルチスレッドは、アップロードの対象外である。
  • ゲームには、FirebaseのUID、認証トークン、メールアドレスを渡さない。
  • 公式の13作品は、独立したHTMLで外部通信なしに動く。
  • アップロードしたゲームにはSDKが自動で追加され、作品専用のプレイヤーIDと作品ごとのクラウドセーブ(JSONで128KiBまで)が使える。

読み取れる点は3つあります。

  1. 「渡さないもの」が先に決まっている。 ログインの認証情報をゲームに渡さないのは、ゲームのコードを信頼しきらない前提の表れと読めます。代わりに渡されるのは「作品専用のプレイヤーID」です。サイト全体の本人情報ではなく、作品ごとに切り分けた識別子という形になっています。
  2. 対象外の三つは、囲いを複雑にする機能と重なる。 サービスワーカーはページの通信を長期間にわたって仲介できる仕組みです。SharedArrayBufferを使うマルチスレッドは、一般にブラウザ側で特別なヘッダー設定(クロスオリジン分離)を求めます。サーバー用コードは、そもそも静的な配信と相性がありません。いずれも、境界の管理を難しくする性質を持ちます。なお、これが除外の理由だったかどうかは、確認済みの事項にありません。
  3. 公式作品は、外部通信を絞っても成り立つ作りになっている。 外部通信なしで動く13作品は、通信先を絞る方針と矛盾しにくい例です。

一方で、確認済みの事項に載っていないことがあります。CSPヘッダーの有無、各項目の許可内容、iframeのsandboxにどの許可が付いているか、SDKと親ページがどう連絡するか、の4点です。ここは「作品を開いて確かめる」部分です。

本物のヘッダーを自分で確かめる手順

実際のゲームの囲いは、ブラウザの開発者ツールで読み取れます。読者が行う手順として示します。

  1. アソバースで作品を開き、開発者ツール(多くのブラウザでF12)のElementsで、ゲームを包むiframeを探します。
  2. iframeの src のホスト名を、アドレスバーのホスト名と見比べます。ホスト名が違えば、別オリジンで配信されていることを自分の目で確認できます。
  3. iframeの sandbox 属性に並ぶ許可の語を、書き写して読みます。
  4. Networkでゲーム本体(index.html)の応答を選び、Response Headersに content-security-policy があるか探します。あれば、項目ごとに許可の範囲を読みます。なければ、その作品ではヘッダーによる制限が付いていないという記録になります。
  5. Consoleに、ブロックを示す赤い文言が出ていないか確認します。

結果の読み方は単純です。ヘッダーが見つかれば、それが「止めている内容」の一次情報になります。見つからなければ、囲いの主役は別オリジンとsandboxで、CSPは使われていないと読むのが妥当です。どちらであっても、記事の推測より実際の表示を優先してください。

公式の案内には、別のかたちで通信を絞っている例も考えられます。たとえばURL投稿の作品は、ゲーム本体が作者側のサーバーから配信されるため、ヘッダーの内容は作者の設定次第です。アップロード作品と同じ囲いだと決めつけず、作品ごとに同じ手順で確認してください。

作る人にとっての実用的な教訓も、ここから引き出せます。外部のライブラリやフォントは、可能なら ZIP の中に入れて相対パスで読み込む形にしておくと、通信を絞られた環境でも動きやすくなります。公開後に画面が真っ白になったら、まずコンソールのブロック文言を探してください。原因がコードか囲いかを切り分ける、最短の入口です。

次の行動

上のHTMLを保存して、ポリシーあり・なしの2回で画面の違いを書き留めてください。そのあと、アソバースの作品を1本開き、iframeのホスト名、sandbox属性、応答ヘッダーの3点を読み取ってみてください。囲いの中身が自分の目で見えると、「ゲームだけに厳しい」という言葉の意味が具体的になります。

参考資料

  • MDN Web Docs「Content Security Policy (CSP)」
  • MDN Web Docs「iframe 要素の sandbox 属性」
  • MDN Web Docs「同一オリジンポリシー」

出典・参考資料