skip to content
usubeni fantasy logo Usubeni Fantasy

同源策略を完全に理解する

/ 17 min read

This Post is Available In: CN EN ES JA

フロントエンド初心者の痛み

私は同一オリジンポリシー(Same-origin policy、以下、略してSOPとします)をフロントエンド初心者の痛みと呼びたいと思います。

まず、同一オリジンとは何かについて簡単に説明します:同一のプロトコル、ホスト、ポートの場合、同一オリジンと見なされます(Same-origin)。例えば、http://example.com:80の場合、プロトコルは http、ホストは example.com、ポートは 80です。

異なるオリジンのリソースにアクセスすると、いくつかの奇妙な制約が発生し、以下で一緒にそれらの場合を詳しく見ていきます。

制約

Canvasの汚染

汚染された(書き込み専用の)キャンバスでは、画像をキャンバスから取り出すことができません。同様の問題がwebGLリソースのロードでも発生します。

Uncaught DOMException: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.

iframe内部の情報の取得

iframe内部のほとんどの情報にアクセスすると、拒否されます。

Uncaught DOMException: Blocked a frame with origin "http://localhost:5000" from accessing a cross-origin frame.

Ajaxリクエストの失敗

最後に、皆が一番よく知っているAjaxリクエストの失敗です。

Access to fetch at 'https://www.baidu.com/' from origin 'http://localhost:5000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

制約のない状況

しかし、同一オリジンポリシーによる制約はいくつかありますが、依然として異なるオリジンのリソースを使用する方法がいくつか存在します:

  • リンク、リダイレクト、フォームの送信
  • <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のリクエストヘッダとレスポンスヘッダを使用して実現されます。具体的なプロセスは以下の通りです。

  1. クロスオリジンのリクエストが発生し、かつ単純リクエストでない場合(そうでなければ実際のリクエストを送信する)
  2. プリフライトリクエストを送信する
  3. サーバーからのCORS設定情報を受け取る
  4. ブラウザが実際のリクエストを送信できるかどうかを判断する
  5. 実際のリクエストを送信し、実際のリクエストのレスポンスヘッダにも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.addHook("preHandler", (req, res, done) => {
const allowedPaths = ["/cors-simple", "/cors"];
console.log(`\n${req.method}: ${req.url}\n`);
if (allowedPaths.includes(req.url)) {
res.header("Access-Control-Allow-Origin", "http://127.0.0.1:3000");
res.header("Access-Control-Allow-Methods", "GET,HEAD,PUT,PATCH,POST,DELETE");
res.header("Access-Control-Allow-Headers", "content-type,custom-header");
res.header("Access-Control-Allow-credentials", "true");
}
const isPreflight = /options/i.test(req.method);
if (isPreflight) {
return res.send();
}
done();
});

より洗練された書き方は 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リクエストヘッダが追加されたリソースだけであり、それ以外は直接リクエストするとエラーが発生し、画像は表示されません

Access to image at 'https://image.api.playstation.com/trophy/np/NPWR13281_00_00A03E8F7ED2727FADE2548E45F2781D32F5D048F6/B81B1B7DBEB337F763D736123661E1D0E8B59FEE.PNG' from origin 'http://localhost:5000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

クロスドメインリクエストで正常に通信できる画像は、キャンバスを汚染せずにキャンバスに追加できます。

リバースプロキシ

CORSを追加するだけでなく、リバースプロキシを使用して、異なるドメインのAPIサービスとウェブアプリケーションファイルを同一ドメインで統合することもできます。

開発環境では、私たちがよく知っているWebpack devServer.proxyViteのServer Optionsを使用できます。

一方、プロダクション環境では、nginxなどのリバースプロキシが一般的です。

postMessage

postMessageは、異なるウィンドウ(例えば、異なるiframe)間のデータ通信を解決するために使用されます:

送信側

targetWindow.postMessage(message, targetOrigin, transfer);

送信側は、postMessageを使用して別のウィンドウに情報を送信するには、まずターゲットウィンドウwindow変数を取得する必要があります。たとえば、iframeの場合、querySelectorで取得し、contentWindowプロパティをアクセスします。window.open()メソッドは、直接ターゲットウィンドウのwindowオブジェクトを返します。

最初の引数は送信するデータで、深いクローンが行われます。同時に、第二の引数 targetOriginを制御することで、ターゲットウィンドウのoriginが指定した値であることを確認できます。

実際の使用例は次のようになります:

main.html
iframe.contentWindow.postMessage({ jsondata: {}, 1: "hello" }, "http://localhost:3000");

受信側

sub.html
window.addEventListener("message", (event) => {
if (event.origin === "http://127.0.0.1:3000") {
console.log("pass");
}
});

この方法は、両者がコードを追加する必要があるため、双方向の信頼できる通信を確保できるからです。最も簡単な方法は、event.originを使用して情報のソースが信頼できるものであることを確認することです。そうでない場合、攻撃を受ける可能性があります。

WebSocket

WebSocketは同一オリジンポリシーを回避することができますが、サーバーの負荷が大きくなります。そのため、同一オリジンポリシーを回避するためにWebSocketを使用してHTTPインタフェースを代替する人はいません。

JSONP

怪しさのテクニック、時代の涙とは、要するにJavaScriptファイルに穴を探して、自由に異なるドメイン間の通信を行うことができることです。関数でラップされたデータを返し、それを定義した関数でデータを読み取ることができます。簡単に言えば、今はほとんど使われていないということです。

要点

  • 同じプロトコル、同じホスト、同じポートは同一オリジンと見なされる
  • 同一オリジンポリシー(SOP)の本質は、異なるオリジンからの情報の読み込みを制限すること
  • SOPはユーザーにより安全なネットワーク環境を提供する
  • 現在の開発者は、主にCORSプロトコルを使用してクロスオリジン制約に対処する
  • CORSをベースにしたフロントエンドの開発者は、まだクロスオリジンのクッキー問題を処理する必要がある
  • クロスオリジンのiframe間の通信にはpostMessageを使用することができる

参考情報

P.S. ブラウザの高速な更新のため、記事で言及されている同一オリジンポリシーは変更される可能性があるので、注意してください。

评论组件加载中……