HTTP / WebSpec

JavaScript が別オリジンを呼べなかった時代から CORS ができるまで

今では、ブラウザの JavaScript から別のドメインの API を fetch で呼ぶのは当たり前です。でも、ほんの 15 年ほど前まで、JavaScript は自分と同じオリジンにしか通信できませんでした。CORS の仕様が「なぜこんな形なのか」は、この時代の事情を知らないとなかなか腑に落ちません。この記事では、CORS ができるまでの経緯をたどります。(私は今回記事にするまで、心の中で「こーず・こるず」と呼んでいたのですが、どうやら「こるす」が正しいようです。)

CORS・同一オリジンポリシー・プリフライトの仕組みそのものは、CORS とプリフライトを「何を守るのか」から理解する にまとめています。あわせて読んでください。

年表

時期出来事
1995 年ごろJavaScript が登場。同じころ、同一オリジンポリシーの考え方が取り入れられる
1999 年Internet Explorer 5 が、JS から HTTP 通信できる XMLHTTP を搭載
2000 年代前半ほかのブラウザも XMLHttpRequest を実装。ただし同じオリジンにしか送れない
2005 年「Ajax」という言葉が生まれ、Google マップなどで JS の通信が一気に広まる
2005 年〜別オリジンを呼びたい需要が増え、JSONP やサーバー側のプロキシなどの回避策が広まる
2009 年ごろ主要なブラウザが、CORS による別オリジンへの XMLHttpRequest に対応し始める
2014 年CORS が W3C の勧告になる(その後、WHATWG の Fetch Standard に統合)
2015 年ごろfetch API が主要なブラウザに入る

年は「ごろ」の目安です。ブラウザごとに対応の時期は少しずつ違います。

JavaScript が生まれたころ:通信の機能はなかった

1995 年ごろに JavaScript が登場したとき、JS にはサーバーと通信する機能がありませんでした。できたのは、ページの中身を動かすことや、フォームの入力チェックくらいです。データを送るには、フォームを送信してページごと読み込み直すしかありませんでした。

同一オリジンポリシーが生まれた理由

同じころ、同一オリジンポリシーの考え方が取り入れられました。きっかけは、JavaScript と、1 つの画面に複数のページを並べるフレームが、ほぼ同じ時期に使えるようになったことです。JS からは、別のウィンドウやフレームに表示されているページの中身(DOM)にアクセスできました。

もし制限がなかったら、次のようなことができてしまいます。

罠サイトのページ
├─ フレームA:罠サイトの JS
└─ フレームB:銀行のサイト(ユーザーはログイン中)
  • フレームA の JS から、フレームB に表示されている残高や取引の履歴を読む
  • ユーザーが入力しているパスワードを読み取る
  • 振込先の欄を書き換える

ユーザーの画面には銀行のページが普通に表示されているので、気づけません。そこで、JS が中身を読んだり書き換えたりできるのは、自分と同じオリジンのページだけ、という制限を入れました。これが同一オリジンポリシーの始まりです。

その後、新しい機能ができるたびに、同じ考え方が当てはめられていきました。

機能同一オリジンポリシーの当てはめ方
フレーム・ウィンドウ別のオリジンのページの中身に触れない
XMLHttpRequest別のオリジンには送れない(のちに CORS で許可できるように)
canvas別のオリジンの画像を描くと、ピクセルを読めなくなる
localStorage などオリジンごとに分けて保存し、ほかのオリジンからは読めない

同一オリジンポリシーは 1 つの仕様というより、「別のオリジンの中身を、ページの JS に渡さない」という考え方で、機能ごとに当てはめられてきたものです。

HTML はもともと別オリジンにリクエストを送れた

一方で、HTML は最初から、別のオリジンにリクエストを送ることができました。

<!-- 別のサイトの画像を表示する(GET) -->
<img src="https://other.example.com/logo.png">

<!-- 別のサイトのスクリプトを読み込む(GET) -->
<script src="https://other.example.com/lib.js"></script>

<!-- 別のサイトにフォームを送信する(POST) -->
<form action="https://other.example.com/post" method="POST">...</form>

画像やスクリプトを別のサーバーから読み込むのは、Web の基本的な使い方です。これを止めることはできません。この「HTML で昔から送れたリクエスト」が、のちの CORS の「単純リクエスト」の元になります。

「埋め込む」はできるが「読む」はできない

昔、他人のサイトの画像を <img> で自分のページに表示する「直リンク」が嫌がられていました。直リンクは今でもできます。同一オリジンポリシーでは止まらないからです。

ただし、埋め込んだ画像を JS で読み取ることはできません。たとえば、別のオリジンの画像を <canvas> に描いてから、ピクセルのデータを JS で取り出そうとすると、ブラウザがエラーにします。

  • 別のオリジンの画像を、ユーザーの画面に表示する(埋め込む)ことはできる
  • その中身を、ページの JS(=そのページの作者)に渡すことはしない

これが同一オリジンポリシーの基本の考え方です。送ること・埋め込むことは昔からできた。止めたのは、中身を読むことだけ——この区別が、のちの CORS や CSRF を理解するうえでの土台になります。

ちなみに、直リンクは転送量や著作権の問題で、セキュリティの問題とは少し性質が違います。直リンクがあまり話題にならなくなったのは、通信の費用が安くなったこと、画像を SNS や CDN に置くことが増えたこと、Referer ヘッダーの確認や CDN のホットリンク保護などで簡単に対策できるようになったことが大きいと思います。最近は、Cross-Origin-Resource-Policy ヘッダーで「このファイルを別のサイトのページに埋め込ませないで」とブラウザに頼むこともできます。

XMLHttpRequest と Ajax の時代:同じオリジンにしか送れない

1999 年、Internet Explorer 5 に、JS から HTTP 通信ができる XMLHTTP という機能が入りました。その後、ほかのブラウザも XMLHttpRequest として同じ機能を実装します。

ここで、ブラウザを作る人たちは悩みます。JS が自由にどこへでも通信して、レスポンスを読めるとしたらどうなるか。

  • ユーザーがログインしている Web メールのデータを、罠サイトの JS が読める
  • 社内ネットワークにあるシステムのデータを、社外の罠サイトの JS が読める

これは危険すぎます。そこで、XMLHttpRequest には同一オリジンポリシーが適用され、同じオリジンにしか送れないことになりました。

2005 年に「Ajax」という言葉が生まれ、ページを読み込み直さずに画面を更新する Web アプリが一気に広まります。ただし、通信できるのは自分のサイトのサーバーだけでした。

この時代にできた「サーバー側の前提」

この時代のサーバーは、次のように考えて作られていました。

  • JSON を送る POST、PUT や DELETE、独自のヘッダーが付いたリクエストは、<form> や <img> では送れない
  • これらを送れるのは XMLHttpRequest だけで、XMLHttpRequest は同じオリジンにしか送れない
  • だから、こうしたリクエストは自分のサイトから来たものだ

実際に、「X-Requested-With ヘッダーが付いていれば、自分のサイトの JS からなので CSRF ではない」と判断するサーバーもありました。この前提が、のちにプリフライトが必要になる理由です。

回避策の時代:みんな工夫して別オリジンを呼んでいた

Web API が増えると、「別のサイトの API を、自分のサイトの JS から呼びたい」という需要が強くなります。地図や検索、SNS の API を組み合わせた「マッシュアップ」が流行した時代です。CORS がまだないので、いろいろな回避策が使われました。

① サーバー側のプロキシ

自分のサイトのサーバーに中継してもらう方法です。

ブラウザの JS → 自分のサイトのサーバー(同じオリジン) → 別のサイトの API

ブラウザから見ると同じオリジンへの通信なので、制限にかかりません。確実ですが、中継のサーバーを用意する手間と負荷がかかります。この方法は今でも、API キーをブラウザに置けない場合などによく使われています。

② JSONP

<script> タグは別のオリジンから読み込める、という性質を利用した方法です。

<script>
  function handleData(data) {
    console.log(data);
  }
</script>
<script src="https://api.example.com/users?callback=handleData"></script>

API のサーバーは、データを JSON ではなく、関数を呼び出す JS として返します。

handleData({ "name": "test" });

読み込まれたスクリプトが実行され、handleData にデータが渡ります。手軽なので広く使われましたが、問題がたくさんありました。

  • 相手のサーバーが返すスクリプトを、そのまま実行する。相手が悪意を持てば、何でもできてしまう
  • GET しか使えない
  • エラーを扱いにくい(失敗したかどうかが分かりにくい)
  • ログイン中のユーザーのデータを返す API を JSONP で提供すると、罠サイトからも同じように読み込まれて、データを抜き取られるおそれがある
③ Flash のクロスドメインポリシー

当時よく使われていた Flash には、サーバーに crossdomain.xml というファイルを置いて、「このドメインからなら、データを読んでいい」と宣言する仕組みがありました。データの持ち主が許可を出すという考え方は、CORS に通じるものです。

④ iframe を使った工夫

iframe と、document.domain や URL の # 以降を使って、別のオリジンのページとデータをやりとりする方法もありました。のちに、HTML5 の postMessage で、ウィンドウ同士が安全にメッセージを送り合えるようになります。

CORS の登場:「安全に」別オリジンを許可する

回避策はどれも、手間がかかるか、安全ではありませんでした。そこで、「ブラウザの標準の仕組みとして、安全に別オリジンへの通信を許可する方法」が検討されます。これが CORS(Cross-Origin Resource Sharing)です。

2009 年ごろから主要なブラウザが対応し始め、2014 年に W3C の勧告になりました。現在は、fetch API を定めた WHATWG の Fetch Standard の一部になっています。

CORS の設計に込められた考え方

CORS の仕様は、ここまでの歴史を踏まえて作られています。

設計理由(歴史とのつながり)
データの持ち主(サーバー)が許可を出す読ませてよいかを決められるのは持ち主だけ。Flash の crossdomain.xml と同じ考え方
許可がなければ、これまでどおり読ませないXMLHttpRequest の時代から守ってきたユーザーの安全は、そのまま守る
単純リクエストは、確認なしで送る<form> や <img> で昔から送れたので、止めても守りにならない
それ以外は、プリフライトで先に確認する「こういうリクエストは自分のサイトからしか来ない」と考えて作られた古いサーバーを、突然危険にしないため
JSONP のようにスクリプトを実行しないデータはデータとして受け取るので、相手のサーバーに何でもされる危険がない

一番大事な考え方は、「新しい機能で、昔はできなかったことを勝手にできるようにしない」です。CORS は、それまでの Web の安全を壊さないように、慎重に穴を開ける仕組みとして作られました。

その後:CORS を補う仕組み

CORS の後も、「単純リクエストは送られてしまう」という弱点(CSRF)を補う仕組みが増えています。CSRF そのものについては、CSRF とは何か:別のサイトから「勝手に送らせる」攻撃と対策 で詳しく説明しています。

  • Cookie の SameSite 属性:別のサイトからのリクエストに Cookie を付けないようにできる。今のブラウザでは、指定がなければ Lax(別のサイトからの POST には付けない)として扱うものが多い
  • Fetch Metadata(Sec-Fetch-Site など):ブラウザが「同じサイトからか、別のサイトからか」をヘッダーで自動で知らせる。サーバーは、別のサイトからの更新リクエストを拒否できる
  • ローカルネットワークへのアクセスの制限:インターネット上のサイトから、社内や自宅のネットワークにアクセスするときに、制限や確認が入るようになってきている

まとめ

  • 同一オリジンポリシーは、JS とフレームが登場したころ、別のサイトのページの中身を JS に読ませないために生まれた。止めたのは「読むこと」で、「送ること・埋め込むこと」は昔からできた。
  • JavaScript は登場したとき通信できず、XMLHttpRequest ができた後も、長いあいだ同じオリジンにしか通信できなかった。
  • その間、HTML の <form> や <img> は昔から別オリジンに送れたので、これが CORS の「単純リクエスト」になった。
  • 「JSON の POST や独自ヘッダーは自分のサイトからしか来ない」と考えて作られたサーバーを守るために、「プリフライト」が作られた。
  • JSONP などの危険な回避策の代わりに、データの持ち主が許可を出す標準の仕組みとして CORS ができた。

この経緯を知ってから仕組みを見直すと、CORS のややこしい仕様にも、それぞれ理由があることがわかります。仕組みの詳しい説明は、CORS とプリフライトを「何を守るのか」から理解する を見てください。

スポンサーリンク
タイトルとURLをコピーしました