コース目次 / 第1章

同一オリジンポリシー — 国境を、見る

APIと、別オリジンのフロントエンドを2つ立て、フロントからAPIを読もうとしてブラウザにブロックされる様子を見ます。自分のフロントすら既定で止まる=CORSが要る動機を体感します。

第1章 / 全5章目安 約10分この章のゴール: SOPが別オリジンのレスポンス読み取りを既定で防ぐ様子を、ブラウザで確かめる

2つのオリジンを立てます。データを返す API(:4000)と、それを使いたい別オリジンのフロントエンド(:4001)。よくある構成です——API と画面が別のドメイン/ポートで動く。まずは CORS を一切設定せず、素の同一オリジンポリシーが働くところを見ます。

法と倫理ゲート

手を動かす前に

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

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

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

API(:4000)

データを返すだけの API です。api.js として保存します。

// api.js — API。127.0.0.1:4000。第1章では CORS ヘッダを付けない。
const http = require('node:http');

const server = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'application/json; charset=utf-8');
  // ここに CORS ヘッダ(Access-Control-Allow-Origin)は付けない。
  // = 同一オリジンポリシーが素のまま効く。
  res.end(JSON.stringify({ secret: '架空太郎の口座残高: 1,234,567 円' }));
});

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

別オリジンのフロント(:4001)

ブラウザから :4000 のデータを読んで表示しようとするページです。front.js として保存します。

// front.js — 別オリジンのフロント。127.0.0.1:4001。ブラウザで :4000 を読もうとする。
const http = require('node:http');

const PAGE = [
  '<!doctype html><meta charset="utf-8"><body style="font-family:sans-serif">',
  '<h1>別オリジンのフロント(:4001)</h1><pre id="out">読み込み中…</pre>',
  '<script>',
  "fetch('http://127.0.0.1:4000/')",
  '  .then(function (r) { return r.json(); })',
  "  .then(function (d) { out.textContent = '読めた: ' + JSON.stringify(d); })",
  "  .catch(function (e) { out.textContent = 'ブロックされた: ' + e; });",
  '</script></body>',
].join('\n');

const server = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
  res.end(PAGE);
});

server.listen(4001, '127.0.0.1', () => console.log('フロント: http://127.0.0.1:4001 (Ctrl+C で停止)'));

両方立てて、ブラウザで開く

2つのターミナルで、それぞれ起動します。

node api.js       # 1つめのターミナル
node front.js     # 2つめのターミナル

そして、ブラウザでフロントを開きます。

http://127.0.0.1:4001

ページには 「ブロックされた: TypeError: Failed to fetch」のように出ます。開発者ツールのコンソールには、こんな趣旨のエラー——「別オリジン 127.0.0.1:4000 のレスポンスに Access-Control-Allow-Origin ヘッダが無いのでブロックした」。
驚くのはここです——これは、あなた自身のフロントエンドです。悪意なんてありません。でもブラウザは、ポートが違うだけで容赦なくブロックしました。SOP は、善意も悪意も区別せず、既定ですべての別オリジン読み取りを止めます。

大事な誤解を、正す

API(:4000)は、実はちゃんと応答しています。ブロックしたのはブラウザです。ブラウザはリクエストを送り、レスポンスも受け取りますが、その中身を、送り元ページの JavaScript に読ませない。
だから——CORS は「サーバを守る」仕組みではありません。実際 curl http://127.0.0.1:4000 なら中身は普通に取れます(curl にオリジンの概念はない)。SOP が守っているのは、「ブラウザ上で、あるサイトのスクリプトが、別オリジンのデータを勝手に読む」こと。守る相手は利用者です。

そして、ここに CORS が要る理由が見えます。

ブラウザは、あなたのフロント(:4001)が「味方」か「攻撃者」かを知りません。だから既定では全部止める。では、どうやって「この :4001 は、私(API)の正規のフロントだから読ませてよい」と伝えるのか——サーバ(API)が、明示的に許可を出す。その許可の仕組みが CORS です。次章で、その“窓口”を開けます。

持ち帰る一言

別オリジンの中身は、既定で読めない。自分のフロントすら止まる。 同一オリジンポリシーは善悪を区別せず、あらゆる別オリジン読み取りを既定で禁じ、利用者を守ります。正規のフロントに読ませたいなら、サーバが明示的に許可を出す——それが CORS です。

こうなっていればOK

卒業まであと3章です。

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