コース目次 / 第3章

CSP が XSS をどう止めるか — 有無で見比べる

反射XSSが残ったページを、CSPあり/なしで開き比べます。script-srcにインラインが無いと、注入されたスクリプトが実行を拒まれる様子をブラウザで見て、CSPが二枚目の壁だと体感します。

第3章 / 全5章目安 約12分この章のゴール: CSPの有無でXSSの実行が変わる様子を見て、多層防御としての位置づけを固める

CSP が「二枚目の壁」だという言葉を、目で確かめます。エスケープを忘れて反射 XSS が残ってしまったページに、CSP を付けたり外したりして、挙動の差を見ます。

法と倫理ゲート

手を動かす前に

このコースは攻撃の技術に触れます。実習してよい相手は、次の3つだけです。

  • (a) 自分で作った練習用のやられ環境(このサイトが配る自作ターゲット)
  • (b) あなた自身の許可された環境(自分の Thousand SKY・secret-notes-ctf など)
  • (c) CTF 競技の中(規約の範囲で)

実在する第三者のシステムを許可なく触ることは、不正アクセス禁止法に触れます。 このゲートは、その線引きを毎回の入口で確かめるためのものです。詳しくは「セキュリティの法と倫理」へ。

標的 — CSP を切り替えられるデモ

下を csp-demo.js として保存します。検索語 q をわざとエスケープせず埋め込む(=残った反射 XSS)ページで、?csp=off を付けると CSP を外せます。

// csp-demo.js — 反射XSSが残ったページに、CSP を付けたり外したりする。127.0.0.1:3700。
const http = require('node:http');

http.createServer((req, res) => {
  const url = new URL(req.url, 'http://127.0.0.1:3700');
  const q = url.searchParams.get('q') || '';
  const cspOn = url.searchParams.get('csp') !== 'off'; // 既定は CSP あり。?csp=off で外す

  if (cspOn) {
    // script-src に 'unsafe-inline' を入れない = インラインの実行を禁じる。
    res.setHeader('Content-Security-Policy',
      "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'");
  }
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
  // 【わざと】q をエスケープせず埋め込む(= 残った反射XSS)。CSP が二枚目の壁になるか見る。
  res.end('<!doctype html><meta charset="utf-8"><body><p>検索語: ' + q + '</p></body>');
}).listen(3700, '127.0.0.1', () => console.log('CSPデモ: http://127.0.0.1:3700 (Ctrl+C で停止)'));
node csp-demo.js

まず、CSP なしで(壁が一枚もない)

ブラウザで、csp=off を付けて、XSS を注入します。

http://127.0.0.1:3700/?csp=off&q=<img src=x onerror=alert('XSS')>

alert(‘XSS’) が出ます。エスケープを忘れているので、注入した <img> の onerror がそのまま実行された。旗艦で見た反射 XSS そのものです。壁が一枚も無い状態。

次に、CSP ありで(二枚目の壁を立てる)

同じ注入を、今度は csp=off を外して開きます(= CSP が効く)。

http://127.0.0.1:3700/?q=<img src=x onerror=alert('XSS')>

今度は alert が出ません。ブラウザの開発者ツールのコンソールを見ると、こんな趣旨の CSP 違反エラーが出ています——「インラインのイベントハンドラの実行を拒否した(script-src に ‘unsafe-inline’ が無いため)」。
注目してほしいのは——注入自体は、成功しています。ページのソースを見れば、<img src=x onerror=…> は HTML の中にちゃんと入っている(エスケープは相変わらず忘れたまま)。でも、CSP が「その onerror を実行するな」とブラウザに指示したので、発火しなかった。これが二枚目の壁です。

だから、CSP はエスケープの「代わり」ではない

この実験の教訓を、正確に言葉にします。

CSP は、XSS の注入を消しはしません。消すのはエスケープ(本命)の仕事。CSP がやったのは、注入されたスクリプトの実行を止めたことだけ。
だから——「CSP を入れたからエスケープは省いてよい」は、間違い。逆に「エスケープしているから CSP は要らない」も、もったいない。本命(エスケープ)で注入を防ぎ、保険(CSP)で万一の実行を止める。二枚重ねて、初めて堅い。片方が破れても、もう片方が残る——それが多層防御です。

確かめ方 — ヘッダが出ているか

CSP が実際にレスポンスに乗っているかは、curl で確かめられます(CSP の“強制”はブラウザの仕事ですが、“宣言”はヘッダに出ます)。

curl -s -i "http://127.0.0.1:3700/?q=test" | grep -i content-security-policy
curl -s -i "http://127.0.0.1:3700/?q=test&csp=off" | grep -i content-security-policy

前者はヘッダが出て、後者は出ません。診断では、この「どんな CSP を宣言しているか」をまず読み、'unsafe-inline' や * のような緩さが無いかを確かめます。

持ち帰る一言

CSP は、注入を消さず、実行を止める。 script-src にインラインが無ければ、エスケープを忘れて注入された onerror も発火しない。ただし CSP は保険であって本命ではない——エスケープ(注入を防ぐ)と CSP(実行を止める)を二枚重ねる。次はまとめて、T3 の Web 診断を締めます。

こうなっていればOK

卒業まであと1章です。

この章はまだ完了していません。