HTTPレスポンスヘッダーとCSP

技術の仕組み | Webセキュリティの基礎

← 技術の仕組み

サーバがブラウザに返すHTTPレスポンスは、いきなり本文(HTML)が来るのではありません。本文の前にヘッダーが付いています。ヘッダーはブラウザへの指示書で、「これはHTML」「このCookieを保存して」「このページで動かしてよいスクリプトはここまで」などを1行ずつ宣言します。その1つがCSP(Content-Security-Policy)で、XSS(クロスサイトスクリプティング)の最後の砦になります。このページでは、管理者が正常に画面を開いた時/CSPが働いた時/スクリプトが実際に動いてしまった時の3シナリオを、ヘッダーの実物で見比べます。

先に押さえる

HTTPレスポンスは 「ステータス行 → ヘッダー群(1行1指示)→ 空行 → 本文」 の順で届きます。ブラウザは本文を表示する前にヘッダーを読み、そこに書かれた指示に従います。だから「本文(HTML)にスクリプトが紛れ込んでいても、ヘッダーが許可していなければ動かない」という守りが成り立ちます。

主要なレスポンスヘッダー(一般名)

中身・保存に関するもの

  • Content-Type:本文が何か(例 text/html; charset=UTF-8)。ブラウザの解釈を決める。
  • Set-Cookie:Cookieを保存させる。属性 HttpOnly(JSから読めない)/Secure(HTTPSのみ送信)/SameSite(別サイトからの送信を制限)が防御の要。

安全化を指示するもの

  • Strict-Transport-Security:以後このサイトは必ずHTTPSで、と強制(HSTS)。
  • X-Content-Type-Optionsnosniff で中身の推測解釈を止める。
  • Content-Security-Policy:実行してよいスクリプトの出所などを宣言。

CSPとは「実行してよいスクリプトの出所の宣言」

CSPは、このページでブラウザが実行してよいスクリプトの出所(ソース)をサーバが宣言するヘッダーです。例えば script-src 'self' は「自分自身(同一オリジン)のスクリプトだけ実行してよい」の意味。ブラウザは読み込もうとする各スクリプトがこの宣言に合うかを判定し、違反するものは実行前にブロックします。default-src 'self' はスクリプトを含む各種リソースの既定の出所を同一オリジンに限定します。

見抜きどころ

CSPが「最後の砦」になるのは、本文(HTML)にスクリプトが注入されても、出所が許可されていなければ動かないから。インラインの <script>…</script> 直書きは既定で禁止され、外部ドメインのスクリプトも 'self' だけなら読み込めません。注入が成功しても実行の一歩手前で止めるのがCSPの働きです。

3シナリオをヘッダーの実物で見る

同じサーバの同じ画面に対して、(A) 正常に開く/(B) 注入スクリプトにCSPが働く/(C) CSPが無く実際に動くの3通りを並べます。違いはたった1行のCSPヘッダーの有無・強さだけ、という点に注目してください。(横に長い行は枠内でスクロールできます)

A管理者が正常に画面を開く
HTTP/1.1 200 OK Content-Type: text/html; charset=UTF-8 Strict-Transport-Security: max-age=31536000 X-Content-Type-Options: nosniff Content-Security-Policy: default-src 'self'; script-src 'self' Set-Cookie: session=ab12...; HttpOnly; Secure; SameSite=Lax <!doctype html> <html> ... 進捗管理画面のHTML(自サイトのJSだけを読み込む) ... </html>

サーバは script-src 'self' で「自分自身のスクリプトだけ実行してよい」と宣言しています。Set-CookieHttpOnly でセッションCookieはJSから読めず、SecureSameSite も付いています。正常な画面なので、本文には自サイトの正規JSしか無く、すべて許可の範囲内で動きます。

B注入されたインラインスクリプトにCSPが働く
HTTP/1.1 200 OK Content-Type: text/html; charset=UTF-8 Content-Security-Policy: default-src 'self'; script-src 'self' Set-Cookie: session=ab12...; HttpOnly; Secure; SameSite=Lax <!doctype html> <html> ... <li>個人タスク<script>fetch('/管理/ロール設定', {method:'POST', body:'is_admin=1'})</script></li> ... </html>

本文には攻撃者が仕込んだインラインスクリプトが紛れ込んでいます(赤字部分)。しかしレスポンスヘッダーのCSPは A と同じ script-src 'self'。インライン実行は許可されていないため、ブラウザは実行前にブロックし、開発者コンソールに違反メッセージを出します。

Refused to execute inline script because it violates the following Content-Security-Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword, a hash, or a nonce is required to enable inline execution.

つまりヘッダーの宣言により、注入スクリプトは「動く前に」止められた。本文に穴(注入)があっても、CSPという1行が最後の砦として機能しています。

CCSPが無い/緩い場合、実際に動いてしまう
HTTP/1.1 200 OK Content-Type: text/html; charset=UTF-8 # Content-Security-Policy ヘッダーが無い(または script-src に 'unsafe-inline' を付けている) Set-Cookie: session=ab12...; SameSite=Lax <!doctype html> <html> ... <li>個人タスク<script>fetch('/管理/ロール設定', {method:'POST', body:'is_admin=1'})</script></li> ... </html>

レスポンスにCSPが無い(または 'unsafe-inline' を付けている)と、注入スクリプトを止めるものがありません。ブラウザは本文中の <script>その画面を開いた閲覧者の権限・セッションで実行します。管理者が開けば、管理者権限で /管理/ロール設定 のような更新が通り、攻撃者を管理者へ昇格させてしまう——という流れです(値・パスは一般化した例)。

つまずきやすい

A・B・C の違いは、CSPという1行のヘッダーの有無・強さだけ。同じ注入・同じ本文でも、script-src 'self' があれば止まり、無ければ動きます。「ヘッダーを見れば、そのページが注入にどれだけ耐えるかが分かる」ということです。

SC向け:試験・実務で差がつく急所

誤解・落とし穴

  • CSPは入力対策(サニタイズ/出力エスケープ)の代わりではない。根本対策は出力時エスケープで、CSPは多層防御の1層
  • script-src 'unsafe-inline' を付けると、注入されたインラインscriptまで許可されCSPの意味が大きく損なわれる
  • アップロードファイルが同一オリジンで配信されると、そこに置いたJSは 'self' 扱いになり <script src> で読み込めてしまう(CSPだけでは塞げない経路)。

効く組み合わせ

  • 出力エスケープで注入自体を無害化(本丸)。
  • CSPは 'self' +必要なら noncehash で最小限に。'unsafe-inline' は避ける。
  • HttpOnly はJSからCookieを読めなくし、XSSでのCookie窃取を防ぐ(CSPとは役割が別の防御)。

判断軸:「注入されても動かせないか(CSP)」と「そもそも注入させないか(エスケープ)」は別の層。試験では両方を混同せず、どの層の話かを見分けられると差がつきます。

つながる仕組み

関連→ ハッシュ・MAC・デジタル署名(守れる範囲の段階) 関連→ アプリ内ブラウザ(WebView)とトークンの受け渡し

この仕組みを使う設問