P112 Origin解体新書 第一回

【SOP:Same Origin Policy】
ユーザーを保護するためにWebに設けられたルール

[SOPがないときー]
社外にある悪意あるサイトを社内からアクセスしてしまった際に、社外の悪意あるサイトに仕掛けられたJavaScript経由で社内のサーバーにアクセスさせられ、そこから取得した情報を再度悪意あるサイトに情報発信するという事ができてしまう。

[SOPの定義]
(Origin)
 URLに含まれるScheme(ex. http, https) + Host + Port番号
 同じホストであっても、SchemeやPort番号が異なれば別Originと解釈

(SOPの実体)
 あるOriginのコンテンツからは、同じOriginのコンテンツにしかアクセスできないという制約

(SOPの注意点)
 異なるOriginへのリクエストは失敗するが、リクエストが行われないわけではない
 異なるOriginへのリクエストが失敗するのは、受信したレスポンスの内容からSOPに違反するとブラウザが判断した場合に、JavaScriptに結果を渡さずにエラーにしているからである。
 URLを確認すれば異なるOriginへのリクエストかどうか分かるため事前に止めれそうであるが、それを行わないのは、サーバーが異なるOriginからのリクエストを許可する隙間を用意できないためである。そのため、ブラウザはレスポンスの内容を確認してから、エラーにするという事を行っている。
 リクエストは送信されているがJavaScriptではエラーになる、ここがSOPおよびCORSを理解する上で重要である。

【CORS:Cross-Origin Resource Sharing】
サービスによってはほかのOriginからアクセスしてきてもよいと明示的に許可したい場合の仕組み

(異なるOriginとの連携)
社内の別Originに埋め込まれているJavaScriptによるアクセスは許容したいが、社外の別Originに埋め込まれているJavaScriptによるアクセスは遮断したい。このような場面において、明示的にアクセスしてもよいOriginを指定するのがCORSという仕組みである。

(Access-Control-Allow-Originレスポンスヘッダー)
異なるOriginにリクエストを送信するときに、ブラウザはOriginヘッダに現在のOriginを付与します。
サーバーはOriginヘッダを確認することで、どのOriginからのリクエストか分かる。
このOriginからのアクセスを許可する場合、サーバーはレスポンスヘッダにAccess-Control-Allow-Originヘッダを付与する。
ブラウザがこのAccess-Control-Allow-Originヘッダを確認し、ここに記載されているOriginに関しては上述した様なエラーとはせずに、レスポンスをJavaScriptに渡す事になる。
上記を纏めると、以下の処理の仕組みが肝である。
 ・情報を取得されるサーバー側でレスポンスヘッダーにAccess-Control-Allow-Originヘッダーというのを埋め込む
 ・リクエストしているブラウザで当ヘッダを解釈し、JavaScriptをエラーにさせていたのを正常処理に変える

(副作用のある処理)
CORSの仕組みがある事で特定の処理のみJavaScriptにレスポンスを渡す事が許されるようになった。
しかし、悪意のあるサーバーで実行されたJavaScriptによる通信自体は発生してしまう。
この通信がGetであれば特段問題ないが、POST/PUT/DELETEといったリクエストであれば、JavaScriptにレスポンスが渡りはしないが、サーバー側のデータ自体が書き換えられてしまう問題が残る。
そのため、Access-Control-Allow-Originを判断してから許可/禁止とかいう以前に、リクエスト自体を禁止させなければならない。
これを解決するために、サービスが今からリクエストしようとしている通信は許可されているのかどうかを事前に確認する仕組みとして、Preflightリクエストという仕組みが用意された。

【Preflight Request】
名前の通り、本リクエスト前に事前に送信されるリクエストである。
サーバーに事前確認を行う前に、ブラウザによって自動で送信される。
Preflightリクエストは、Optionsメソッドが用いられる。
(理由:Getなどで実現すると、サーバーに配置しているアプリケーションで冪等性を守らない処理を実装している危険性が拭えないため)

(OPTIONSメソッド)
ブラウザはプリフライトリクエストを送信する際に、Access-Control-Request-Methodというリクエストヘッダーに、どのメソッド(ex. POST, DELETE)を送信するかを含めた状態で送信を行う。
これにより、ブラウザから情報を抜き取りたいサーバーに対して、JavaScriptから送信されてきたリクエストは許可される通信かどうかを事前に照会することができる。

(Access-Control-Allow-Methods)
プリフライトリクエストによって発生した通信に対して、サーバーが許可を行う場合、Access-Control-Allow-Originレスポンスヘッダーに加え、Access-Control-Allow-Methodsというレスポンスヘッダーを加えた状態で、ブラウザにレスポンスを返す。
その後は、実処理のリクエストが行われる。
もし、サーバー側が許可していない場合は、Access-Control-Allow-Originレスポンスヘッダーをプリフライトリクエストのレスポンスで返却を行わないため、ブラウザはプリフライトの時点で許可されない事に気づけるので、実処理のリクエストは送信されない。結果、POSTやDELETEなどの通信が防ぐことができる。

(Access-Control-Max-Age)
毎回プリフライトリクエストを送信すると、単純にリクエストが2倍になってしまうため、サーバーに負荷がかかってしまう。
そのため、サーバーは最初のプリフライトリクエストのレスポンスに対して、Access-Control-Max-Ageレスポンスヘッダーというのを付加できる。このヘッダーが付加されていると、その秒数だけはAccess-Control-Allow-MethodsとAccess-Control-Allow-Headersの情報をキャッシュできる。なお、デフォルト値は5秒なので、明示的に当ヘッダーを指定しない場合は5秒間はプリフライトリクエストは省略される。

【ヘッダと資格情報】
JavaScriptからのCORSリクエストを想定していないサーバーとの互換性を保つため、レスポンスヘッダへのアクセスやCookieの付与については、デフォルトで制限が課されている。CORSに対応する場合、明示的なヘッダ設定を行うことで、これら制限を緩和できる。

(Access-Control-Expose-Headers)
JavaScriptからアクセスすると、デフォルトでは全てのヘッダにアクセスできる訳ではない。
HTTPヘッダーはさまざまな中継サーバーによって追加されることがあり、重要な情報を含む。それらヘッダーはJavaScriptから取得されることを想定していないものが多く、脆弱性の原因になる可能性がある。
よって、JavaScriptからアクセス可能なヘッダを限定する必要があり、安全である最小限のヘッダー(Content-Languageなど)をホワイトリスト(CORS-Safelisted Response Header)として定義し、それ以外(Cookieなど)は明示的な許可がないと取得できないように制限がなされることとなった。
もし、ホワイトリスト以外のヘッダーをJavaScriptに提供したければ、サーバーでAccess-Control-Expose-Headersを付与することで解除できる。なお、ここにワイルドカード(*)指定をすれば、すべてのヘッダーを提供できる。なお、Authorizationレスポンスヘッダーのみ、別途定義する必要がある。

(Access-Control-Allow-Credentials)
ブラウザからのリクエストにはCookieが自動で付与される。このCookieにより、サーバーはリクエストを送った相手が一定の資格を持つと判断するため、CookieはCredential(資格情報)とも呼ばれる。
しかし、CORSの場合はCookieが自動で付与されると問題になる場合がある。そこで、安全側に倒すため、CORSリクエストにはCookieが自動で付与されない。もし、CORSリクエストでCookieの送信を許可する場合は、明示的にCookieを指定する必要がある。
サーバー側も、Cookieを含んだCORSリクエストへのレスポンスにAccess-Control-Allow-Credentials:trueを付与し、Cookieを必要とするレスポンスに対するアクセスを明示的に許可する必要がある。このヘッダーが付与された場合は、ほかのAccess-Control系ヘッダーの値はワイルドカード指定ではなく、すべて明示的に値を列挙する必要がある。

【Preflight Requestが送信される条件】
GETでもプリフライトリクエストが必要で、POSTなどでプリフライトリクエストが不要なケースもある。

(Simple Requestとは何か)
基本的には、仕様で定義された「許容できるHTTPメソッド(CORS-Safelisted Method)」および「許容できるHTTPヘッダー(CORS-Safelisted Request Header)」を満たしている場合、プリフライトリクエストは発生しない。
プリフライトリクエストが不要なリクエストは、シンプルリクエストと呼ばれることがある。
シンプルリクエストは、CORSがFetch(※1)の仕様から独立していた時代に使われていた用語で、CORSの仕様がFetchにマージされたため、現在この用語は仕様では使われていない。但し、仕様策定の場面では今でも登場することがある。
ブラウザがすでにデフォルトで送信できるリクエストと同じであればプリフライトリクエストしなくても問題ないことになる。このブラウザがデフォルトで送信できるリクエストをシンプルリクエストと呼ぶ。
主なリクエストは以下の通りである。
 ・a, img, script, linkなどから発生するGET
 ・formから発生するGET/POST
但し、formでは起こり得ないリクエスト、例えばContent-Type:application/jsonや独自のリクエストヘッダーが付与されているとシンプルリクエストではなくなってしまい、プリフライトリクエストが発生する。
また、PUTやDELETEもformでは投げれないため、必ずプリフライトが送られる。
なお、シンプルリクエストもCORSであるため、Originヘッダーは付与される。ブラウザはAccess-Control-Allow-Originレスポンスヘッダーをチェックし、結果をJavaScriptに渡すかどうかは判断する点は変わりない。

※1  Fetch を使う - Web API | MDN

【SOPを迂回する危険なハック】
SOPを迂回するハックは推奨されない。

(JSONPによる迂回)
scriptタグはJavaScriptを取得するためにGETを発生させるが、たとえ異なるOriginを記述してもCORSの対象にはならない。これを利用してSOPを迂回する方法がJSONPである。
取得したいデータを関数実行で囲んで(Padding)、JavaScriptとして返すことでOriginをまたいたデータのやりとりをCORSなしで行う手法がJSONP(JSON Padding)である。
しかし、任意のOriginから取得できるということは、攻撃サイトからも同じ方法で取得できることを意味する。Cookieで制限しているつもりでも、リクエストにはCookieが自動で付与され、防御になrない。
Originをまたいだデータを提供する場合は、OriginリクエストヘッダーをもとにAccess-Control-Allow-Originレスポンスヘッダーを正しく返却して取得元を制限するようにCORSをきちんとハンドリングする必要がある。

【document.domainによる迂回】
紙面参照

当文章は自分で保持している上記雑誌を引用する時のメモのため、多くを割愛して書いております。(それでも長い文になっていますが、当連載自体非常にページ量多いので)
きちんと当内容を理解したい人は、図やコードも提示されており、きちんと文章になっている当雑誌を購入して読まれる事を強くお勧めします。
あまりこの手の内容が纏まった記事はあっても、紙媒体が無かったので非常に助かります。