フロントエンド初心者の痛み
私は同一オリジンポリシー(Same-origin policy、以下、略してSOPとします)をフロントエンド初心者の痛みと呼びたいと思います。
まず、同一オリジンとは何かについて簡単に説明します:同一のプロトコル、ホスト、ポートの場合、同一オリジンと見なされます(Same-origin)。例えば、http://example.com:80
の場合、プロトコルは http
、ホストは example.com
、ポートは 80
です。
異なるオリジンのリソースにアクセスすると、いくつかの奇妙な制約が発生し、以下で一緒にそれらの場合を詳しく見ていきます。
制約
Canvasの汚染
汚染された(書き込み専用の)キャンバスでは、画像をキャンバスから取り出すことができません。同様の問題がwebGLリソースのロードでも発生します。
iframe内部の情報の取得
iframe内部のほとんどの情報にアクセスすると、拒否されます。
Ajaxリクエストの失敗
最後に、皆が一番よく知っているAjaxリクエストの失敗です。
制約のない状況
しかし、同一オリジンポリシーによる制約はいくつかありますが、依然として異なるオリジンのリソースを使用する方法がいくつか存在します:
- リンク、リダイレクト、フォームの送信
<script src="…"></script>
<link rel="stylesheet" href="…">
<img>
、<video>
、<audio>
、<object>
、<embed>
タグ- @font-face で適用されるフォント
- iframeのロード(
X-Frame-Options
でブロックされることがあります)
ではなぜ、厳格な同一オリジンポリシーですが、上記の方法でデータを取得できるのでしょうか?実は、その本質は次のようなものです。
本質
実は、すべての制約のない状況はデータの取得しかできず、データの変更はできません。つまり、遠くから見ることはできますが、それに触れることはできません。
上記で述べたすべての方法は、読み取り専用のもので、取得したものを変更することはできません。iframeでページを開くと、完全なページを見ることができますが、このページのプログラムを操作することは絶対にできません。
その中でもフォームのアクションは、過去の遺産問題であり、POSTでデータを送信する操作と思われるかもしれませんが、実際には、返されたデータにアクセスすることはできません。また、送信後には直接ターゲットのウェブアドレスにリダイレクトされ、リンクやリダイレクトと同じカテゴリーに属します。かつては、PHPの時代にこれをよく使用しました。
したがって、Ajaxリクエストに対しても、本質的には非同一オリジンのリソースに対する書き込みは許可されません(opaque response)。リクエスト自体は成功しますが、ブラウザは取得したデータを操作することを拒否します。言い換えれば、異なるオリジンへのリクエスト結果のインターセプトは、ブラウザの動作によるものです。
なぜ存在するのか
本質がわかれば、なぜSOPが存在するのかを簡単に推測できます。
- オンラインセキュリティの問題は、主にクロスサイトリクエストフォージェリ(CSRF)攻撃を防ぐことを目的としています。
- iframe へのホストの不正な操作を防止します。
- クライアント側で、ソースの異なる画像、音声、動画ファイルの編集を禁止します。
対処策
同一生成元ポリシー(SOP)はセキュリティ上の利点をもたらしますが、開発体験にはやや不便があります。開発者はSOPへの対応のために追加の努力を要することになります。
CORSプロトコル
CORSプロトコルは、異なるオリジン間での応答の共有を許可し、HTMLのフォーム要素では実現できないが、より多目的なフェッチを可能にするために存在します。これはHTTPの上に重ねられたプロトコルであり、応答が他のオリジンと共有できることを宣言することができます。
CORSプロトコルは、リソースの異なるオリジンからの読み取りが許可されるかどうかを調整するために使用されます。
それは、CORSがプロトコルであるということは間違いありません。これはHTTPプロトコルの上に構築され、HTTPのリクエストヘッダとレスポンスヘッダを使用して実現されます。具体的なプロセスは以下の通りです。
- クロスオリジンのリクエストが発生し、かつ単純リクエストでない場合(そうでなければ実際のリクエストを送信する)
- プリフライトリクエストを送信する
- サーバーからのCORS設定情報を受け取る
- ブラウザが実際のリクエストを送信できるかどうかを判断する
- 実際のリクエストを送信し、実際のリクエストのレスポンスヘッダにもCORS設定情報が含まれるため、ブラウザは開発者がレスポンスデータを使用できるように許可します(単純なリクエストの場合は直接送信されますが、設定情報をチェックしてエラーが発生した場合はクロスオリジンエラーが報告されます)。
プリフライトリクエスト(通常はプレフライトリクエストとも呼ばれる)は、OPTIONS
メソッドのHTTPリクエストで、以下の2つの重要なリクエストヘッダがあります。
Access-Control-Request-Method
:実際のリクエストのメソッドAccess-Control-Request-Headers
:実際のリクエストに含まれるヘッダー
CORSプロトコルは、HTTPレスポンスヘッダを介してクロスオリジンが許可される条件を返します。名前から推測すると、これらのレスポンスヘッダがCORSプロトコルで果たす役割が基本的に理解できます。
Access-Control-Allow-Methods
:許可されるメソッドAccess-Control-Allow-Headers
:許可されるヘッダーAccess-Control-Allow-Origin
:アクセスが許可されるソースAccess-Control-Allow-Credentials
:認証情報を含めてアクセスを許可するかどうかAccess-Control-Max-Age
:上記2つの情報のキャッシュ時間Access-Control-Expose-Headers
:JavaScriptが読み取ることのできるレスポンスヘッダー
では、なぜプリフライトリクエストが必要なのでしょうか?
私の理解によると、先に述べたように、ブラウザの動作は結果をブロックすることです。リクエストは依然としてサーバーに正常に送信され、通常のロジックが実行されます。これは非常に危険です。しかし、プリフライトリクエストがあれば、実際のリクエストが送信される前に中断されます。また、CORSプロトコルは単純なリクエストをサーバーに到達させるのを防ぎません。これはgetメソッドがデータの変更をもたらさないため、深刻な結果を引き起こすのが難しいため、また、歴史的な理由もあるかもしれません。
以下は、CORSプロトコルのサーバーサイドの簡易実装の例です。
より洗練された書き方は fastify-cors を参考にしてください。
CORSプロトコルが正しく交渉されると、ブラウザはJavaScriptで外部リソースにアクセスすることを許可します。しかし、問題はこれで終わりではありません(これが私が開発の経験があまり良くないと言っている理由です)。外部リソースの読み取りができるようになっても、クッキーや他の認証情報にまだ問題があります:
デフォルトでは、fetch
メソッドはクロスオリジンクッキーを送信せず、レスポンスのset-cookie
を設定しません。これにはcredentials: "include"
を追加する必要がありますが、その後も以下の設定が必要です:
Access-Control-Allow-Credentials
を追加すると、外部リソースへのリクエストに認証情報を携帯できるようになりますAllow-Credentials
を構成した後、Access-Control-Allow-Origin
は*
ではなく、単一のOrigin
である必要があります- 送信するためには
SameSite=None
のクッキーである必要があります SameSite=None
の場合、Secure
も必要ですhttps
プロトコルである必要があります。そうでない場合、Secure
クッキーを書き込むことができません
P.S. Same SiteとSame Originには微妙な違いがありますが、ほとんどの場合考慮する必要はありません。詳細はこちらを参照してください:The great SameSite confusion
Chrome80(2020年2月)以降、Set-Cookie
ヘッダのSameSite
はデフォルトでLax
に設定されるようになりました。そのため、ユーザーはアップグレード後にログインできなくなると感じるかもしれませんが、これはクロスオリジンクッキーがデフォルトで送信されなくなったためです。
ちなみに、CORSと同様の効果を持つリクエストヘッダにはCORPがあります。このリクエストヘッダは、デフォルトでcross-origin
に設定されているため、<script>
や<img>
などのリソースの参照を禁止します。
Today, browsers act as though Cross-Origin-Resource-Policy: cross-origin is set on every response that lacks an explicit CORP header.
クリーンなキャンバス
CORSプロトコルは、キャンバスからの画像読み取りの問題を解決するためにも使用できます。デフォルトでは、画像リクエストはCORSではありませんので、img.crossOrigin = 'anonymous'
を追加する必要があります(JavaScriptで追加する場合はキャメルケースに注意してください。全小文字では機能しませんが、HTMLタグに追加する場合は全小文字です)。
以前とは異なるのは、キャンバスがクロスドメインの画像を直接表示できるはずだったが、crossOrigin
を追加すると、正常にリクエストできるのはCORSリクエストヘッダが追加されたリソースだけであり、それ以外は直接リクエストするとエラーが発生し、画像は表示されません:
クロスドメインリクエストで正常に通信できる画像は、キャンバスを汚染せずにキャンバスに追加できます。
リバースプロキシ
CORSを追加するだけでなく、リバースプロキシを使用して、異なるドメインのAPIサービスとウェブアプリケーションファイルを同一ドメインで統合することもできます。
開発環境では、私たちがよく知っているWebpack devServer.proxyやViteのServer Optionsを使用できます。
一方、プロダクション環境では、nginxなどのリバースプロキシが一般的です。
postMessage
postMessageは、異なるウィンドウ(例えば、異なるiframe)間のデータ通信を解決するために使用されます:
送信側
送信側は、postMessage
を使用して別のウィンドウに情報を送信するには、まずターゲットウィンドウのwindow
変数を取得する必要があります。たとえば、iframeの場合、querySelector
で取得し、contentWindow
プロパティをアクセスします。window.open()
メソッドは、直接ターゲットウィンドウのwindow
オブジェクトを返します。
最初の引数は送信するデータで、深いクローンが行われます。同時に、第二の引数 targetOrigin
を制御することで、ターゲットウィンドウのoriginが指定した値であることを確認できます。
実際の使用例は次のようになります:
受信側
この方法は、両者がコードを追加する必要があるため、双方向の信頼できる通信を確保できるからです。最も簡単な方法は、event.origin
を使用して情報のソースが信頼できるものであることを確認することです。そうでない場合、攻撃を受ける可能性があります。
WebSocket
WebSocketは同一オリジンポリシーを回避することができますが、サーバーの負荷が大きくなります。そのため、同一オリジンポリシーを回避するためにWebSocketを使用してHTTPインタフェースを代替する人はいません。
JSONP
怪しさのテクニック、時代の涙とは、要するにJavaScriptファイルに穴を探して、自由に異なるドメイン間の通信を行うことができることです。関数でラップされたデータを返し、それを定義した関数でデータを読み取ることができます。簡単に言えば、今はほとんど使われていないということです。
要点
- 同じプロトコル、同じホスト、同じポートは同一オリジンと見なされる
- 同一オリジンポリシー(SOP)の本質は、異なるオリジンからの情報の読み込みを制限すること
- SOPはユーザーにより安全なネットワーク環境を提供する
- 現在の開発者は、主にCORSプロトコルを使用してクロスオリジン制約に対処する
- CORSをベースにしたフロントエンドの開発者は、まだクロスオリジンのクッキー問題を処理する必要がある
- クロスオリジンのiframe間の通信には
postMessage
を使用することができる
参考情報
- 同一オリジンポリシー(Same-origin policy)
- 画像とキャンバスのクロスオリジン利用を許可
- WebGLでのテクスチャの使用
- 同じサイト(SameSite)の混乱について
- Webページの読み込み
- Maestro向けリファレンス
P.S. ブラウザの高速な更新のため、記事で言及されている同一オリジンポリシーは変更される可能性があるので、注意してください。