2022年10月18日

Fetch: クロスオリジンリクエスト

別のウェブサイトにfetchリクエストを送信すると、おそらく失敗します。

たとえば、http://example.comを取得してみましょう。

try {
  await fetch('http://example.com');
} catch(err) {
  alert(err); // Failed to fetch
}

予想通り、Fetchは失敗します。

ここでの中心となる概念は、*オリジン* - ドメイン/ポート/プロトコルのトリプレットです。

クロスオリジンリクエスト - 別のドメイン(サブドメインでも)、プロトコル、またはポートに送信されるリクエスト - には、リモート側からの特別なヘッダーが必要です。

そのポリシーは「CORS」と呼ばれます:クロスオリジンリソースシェアリング。

CORSが必要な理由:簡単な歴史

CORSは、インターネットを悪意のあるハッカーから保護するために存在します。

本当に。簡単な歴史的余談をしましょう。

長年、あるサイトのスクリプトは別のサイトのコンテンツにアクセスできませんでした。

そのシンプルでありながら強力なルールは、インターネットセキュリティの基盤でした。たとえば、ウェブサイトhacker.comの悪意のあるスクリプトは、ウェブサイトgmail.comのユーザーのメールボックスにアクセスできませんでした。人々は安全を感じました。

JavaScriptにも、当時はネットワークリクエストを実行するための特別なメソッドはありませんでした。それはウェブページを装飾するためのおもちゃの言語でした。

しかし、Web開発者はより多くの力を要求しました。制限を回避し、他のウェブサイトにリクエストを行うために、さまざまなトリックが発明されました。

フォームの使用

別のサーバーと通信する1つの方法は、そこに<form>を送信することでした。人々はそれを<iframe>に送信し、現在のページにとどまるようにしました。このように:

<!-- form target -->
<iframe name="iframe"></iframe>

<!-- a form could be dynamically generated and submitted by JavaScript -->
<form target="iframe" method="POST" action="http://another.com/…">
  ...
</form>

そのため、フォームはどこにでもデータを送信できるため、ネットワークメソッドがなくても、別のサイトにGET / POSTリクエストを行うことができました。ただし、別のサイトから<iframe>のコンテンツにアクセスすることは禁止されているため、レスポンスを読み取ることができませんでした。

正確に言うと、実際にはそのためのトリックがあり、iframeとページの両方に特別なスクリプトが必要でした。そのため、iframeとの通信は技術的に可能でした。今は詳細に入る意味がないので、これらの恐竜を安らかに眠らせておきましょう。

スクリプトの使用

もう1つのトリックは、scriptタグを使用することでした。スクリプトは、<script src="http://another.com/…">のように、任意のドメインを持つ任意のsrcを持つことができます。任意のウェブサイトからスクリプトを実行できます。

たとえば、another.comなどのウェブサイトがこの種のアクセスのためのデータを公開することを目的としている場合、いわゆる「JSONP(パディング付きJSON)」プロトコルが使用されました。

仕組みは次のとおりです。

たとえば、私たちのサイトで、天気などのhttp://another.comからデータを取得する必要があるとしましょう。

  1. まず、事前に、データを受け入れるグローバル関数を宣言します。たとえば、gotWeatherです。

    // 1. Declare the function to process the weather data
    function gotWeather({ temperature, humidity }) {
      alert(`temperature: ${temperature}, humidity: ${humidity}`);
    }
  2. 次に、関数の名前をcallback URLパラメータとして使用して、src="http://another.com/weather.json?callback=gotWeather"を持つ<script>タグを作成します。

    let script = document.createElement('script');
    script.src = `http://another.com/weather.json?callback=gotWeather`;
    document.body.append(script);
  3. リモートサーバーanother.comは、私たちに受信させたいデータを使用してgotWeather(...)を呼び出すスクリプトを動的に生成します。

    // The expected answer from the server looks like this:
    gotWeather({
      temperature: 25,
      humidity: 78
    });
  4. リモートスクリプトがロードされて実行されると、gotWeatherが実行され、それが私たちの関数であるため、データが得られます。

これは機能し、セキュリティに違反しません。なぜなら、両方がこの方法でデータを渡すことに同意したからです。そして、両方が同意すれば、それは間違いなくハックではありません。非常に古いブラウザでも機能するため、そのようなアクセスを提供するサービスはまだあります。

しばらくして、ネットワークメソッドがブラウザのJavaScriptに登場しました。

最初は、クロスオリジンリクエストは禁止されていました。しかし、長い議論の結果、クロスオリジンリクエストは許可されましたが、新しい機能には、特別なヘッダーで表されるサーバーによる明示的な許可が必要でした。

安全なリクエスト

クロスオリジンリクエストには2つのタイプがあります。

  1. 安全なリクエスト。
  2. その他すべて。

安全なリクエストは作成が簡単なので、そこから始めましょう。

リクエストは、次の2つの条件を満たす場合に安全です。

  1. 安全なメソッド:GET、POST、またはHEAD
  2. 安全なヘッダー - 許可されるカスタムヘッダーは次のとおりです。
    • Accept,
    • Accept-Language,
    • Content-Language,
    • 値がapplication/x-www-form-urlencodedmultipart/form-data、またはtext/plainContent-Type

その他のすべてのリクエストは「安全でない」と見なされます。たとえば、PUTメソッドまたはAPI-Key HTTPヘッダーを使用したリクエストは、制限に適合しません。

本質的な違いは、安全なリクエストは、特別なメソッドなしで、<form>または<script>を使用して行うことができるということです。

そのため、非常に古いサーバーでも安全なリクエストを受け入れる準備ができていなければなりません。

それとは反対に、標準以外のヘッダーや、たとえばメソッドDELETEを使用したリクエストは、この方法では作成できません。長い間、JavaScriptはこのようなリクエストを実行できませんでした。そのため、古いサーバーは、そのようなリクエストが特権ソースからのものであると想定している場合があります。「Webページはそれらを送信できないため」です。

安全でないリクエストを作成しようとすると、ブラウザは特別な「プリフライト」リクエストを送信し、サーバーにそのようなクロスオリジンリクエストを受け入れるかどうかを尋ねます。

そして、サーバーがヘッダーで明示的に確認しない限り、安全でないリクエストは送信されません。

では、詳細を見ていきましょう。

安全なリクエストのCORS

リクエストがクロスオリジンの場合、ブラウザは常にOriginヘッダーを追加します。

たとえば、https://javascriptinfo.dokyumento.jp/pageからhttps://anywhere.com/requestをリクエストする場合、ヘッダーは次のようになります。

GET /request
Host: anywhere.com
Origin: https://javascriptinfo.dokyumento.jp
...

ご覧のとおり、Originヘッダーには、パスなしで、オリジン(ドメイン/プロトコル/ポート)が正確に含まれています。

サーバーはOriginを検査し、そのようなリクエストを受け入れることに同意する場合、特別なヘッダーAccess-Control-Allow-Originをレスポンスに追加します。そのヘッダーには、許可されたオリジン(この場合はhttps://javascriptinfo.dokyumento.jp)またはスター*が含まれている必要があります。その後、レスポンスは成功し、そうでない場合はエラーになります。

ブラウザはここで信頼できる仲介者の役割を果たします。

  1. 正しいOriginがクロスオリジンリクエストで送信されることを保証します。
  2. レスポンスで許可するAccess-Control-Allow-Originがあるかどうかを確認し、存在する場合はJavaScriptがレスポンスにアクセスすることを許可し、そうでない場合はエラーで失敗します。

許可的なサーバーレスポンスの例を次に示します。

200 OK
Content-Type:text/html; charset=UTF-8
Access-Control-Allow-Origin: https://javascriptinfo.dokyumento.jp

レスポンスヘッダー

クロスオリジンリクエストの場合、デフォルトではJavaScriptはいわゆる「安全な」レスポンスヘッダーにのみアクセスできます。

  • Cache-Control
  • Content-Language
  • Content-Length
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

他のレスポンスヘッダーにアクセスするとエラーが発生します。

JavaScriptに他のレスポンスヘッダーへのアクセスを許可するには、サーバーはAccess-Control-Expose-Headersヘッダーを送信する必要があります。アクセス可能にする必要がある安全でないヘッダー名のコンマ区切りリストが含まれています。

例えば

200 OK
Content-Type:text/html; charset=UTF-8
Content-Length: 12345
Content-Encoding: gzip
API-Key: 2c9de507f2c54aa1
Access-Control-Allow-Origin: https://javascriptinfo.dokyumento.jp
Access-Control-Expose-Headers: Content-Encoding,API-Key

このようなAccess-Control-Expose-Headersヘッダーを使用すると、スクリプトはレスポンスのContent-EncodingおよびAPI-Keyヘッダーを読み取ることができます。

「安全でない」リクエスト

任意のHTTPメソッドを使用できます。GET/POSTだけでなく、PATCHDELETEなども使用できます。

少し前までは、Webページがこのようなリクエストを行うことができるとは誰も想像できませんでした。そのため、標準以外のメソッドを「これはブラウザではない」というシグナルとして扱うWebサービスがまだ存在する可能性があります。アクセス権を確認するときに、それを考慮に入れることができます。

そのため、誤解を避けるために、ブラウザは「安全でない」リクエスト(昔はできなかったリクエスト)をすぐに実行しません。最初に、許可を求めるための予備的な、いわゆる「プリフライト」リクエストを送信します。

プリフライトリクエストは、メソッドOPTIONS、本文なし、および3つのヘッダーを使用します。

  • Access-Control-Request-Methodヘッダーには、安全でないリクエストのメソッドがあります。
  • Access-Control-Request-Headersヘッダーは、その安全でないHTTPヘッダーのコンマ区切りリストを提供します。
  • Originヘッダーは、リクエストの送信元を示します。(https://javascriptinfo.dokyumento.jpなど)

サーバーがリクエストを提供することに同意する場合、空の本文、ステータス200、およびヘッダーで応答する必要があります。

  • Access-Control-Allow-Originは、*またはリクエスト元のオリジン(https://javascriptinfo.dokyumento.jpなど)のいずれかである必要があります。
  • Access-Control-Allow-Methodsには、許可されたメソッドが必要です。
  • Access-Control-Allow-Headersには、許可されたヘッダーのリストが必要です。
  • さらに、ヘッダーAccess-Control-Max-Ageは、権限をキャッシュする秒数を指定できます。そのため、ブラウザは、指定された権限を満たす後続のリクエストに対してプリフライトを送信する必要がなくなります。

クロスオリジンPATCHリクエストの例で、段階的に動作を確認してみましょう(このメソッドは多くの場合、データの更新に使用されます)。

let response = await fetch('https://site.com/service.json', {
  method: 'PATCH',
  headers: {
    'Content-Type': 'application/json',
    'API-Key': 'secret'
  }
});

リクエストが安全でない理由は3つあります(1つで十分です)。

  • メソッドPATCH
  • Content-Typeは、application/x-www-form-urlencodedmultipart/form-datatext/plainのいずれでもありません。
  • 「安全でない」API-Keyヘッダー。

ステップ1(プリフライトリクエスト)

このようなリクエストを送信する前に、ブラウザは独自に、次のようなプリフライトリクエストを送信します。

OPTIONS /service.json
Host: site.com
Origin: https://javascriptinfo.dokyumento.jp
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: Content-Type,API-Key
  • メソッド: OPTIONS.
  • パス – メインリクエストと全く同じです: /service.json.
  • クロスオリジン特有のヘッダー
    • Origin – ソースオリジン。
    • Access-Control-Request-Method – リクエストされたメソッド。
    • Access-Control-Request-Headers – カンマ区切りの「安全でない」ヘッダーのリスト。

ステップ2 (プリフライトレスポンス)

サーバーはステータス200と以下のヘッダーで応答する必要があります

  • Access-Control-Allow-Origin: https://javascriptinfo.dokyumento.jp
  • Access-Control-Allow-Methods: PATCH
  • Access-Control-Allow-Headers: Content-Type,API-Key.

これにより、今後の通信が許可されます。許可されない場合はエラーが発生します。

サーバーが今後他のメソッドやヘッダーを期待する場合は、それらをリストに追加して事前に許可しておくことが理にかなっています。

例えば、このレスポンスはPUTDELETE、追加のヘッダーも許可します

200 OK
Access-Control-Allow-Origin: https://javascriptinfo.dokyumento.jp
Access-Control-Allow-Methods: PUT,PATCH,DELETE
Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control
Access-Control-Max-Age: 86400

ブラウザは、PATCHAccess-Control-Allow-Methods に含まれており、Content-Type,API-KeyAccess-Control-Allow-Headers のリストに含まれていることを確認できるため、メインリクエストを送信します。

秒数を示す Access-Control-Max-Age ヘッダーが存在する場合、プリフライトの許可は指定された時間キャッシュされます。上記のレスポンスは86400秒(1日)キャッシュされます。この期間内は、後続のリクエストでプリフライトは発生しません。キャッシュされた許可に適合する場合、リクエストは直接送信されます。

ステップ3 (実際のリクエスト)

プリフライトが成功すると、ブラウザはメインリクエストを実行します。ここでのプロセスは、安全なリクエストの場合と同じです。

メインリクエストには、Origin ヘッダーが含まれています(クロスオリジンであるため)。

PATCH /service.json
Host: site.com
Content-Type: application/json
API-Key: secret
Origin: https://javascriptinfo.dokyumento.jp

ステップ4 (実際のリクエスト)

サーバーは、メインレスポンスに Access-Control-Allow-Origin を追加することを忘れてはなりません。プリフライトが成功しても、この義務は免除されません。

Access-Control-Allow-Origin: https://javascriptinfo.dokyumento.jp

その後、JavaScriptはメインサーバーのレスポンスを読み取ることができます。

注意点

プリフライトリクエストは「バックグラウンドで」発生し、JavaScriptには見えません。

JavaScriptは、メインリクエストへのレスポンス、またはサーバーの許可がない場合はエラーのみを取得します。

クレデンシャル

JavaScriptコードによって開始されたクロスオリジンリクエストは、デフォルトではクレデンシャル(CookieまたはHTTP認証)を持ちません。

これはHTTPリクエストでは一般的ではありません。通常、http://site.comへのリクエストには、そのドメインからのすべてのCookieが添付されます。一方、JavaScriptメソッドによって行われたクロスオリジンリクエストは例外です。

例えば、fetch('http://another.com') は、another.com ドメインに属するCookieであっても (!) Cookieを送信しません。

なぜでしょうか?

これは、クレデンシャル付きのリクエストが、クレデンシャルなしのリクエストよりもはるかに強力であるためです。許可された場合、JavaScriptはユーザーに代わって行動し、ユーザーのクレデンシャルを使用して機密情報にアクセスする完全な権限を与えられます。

サーバーは本当にスクリプトをそれほど信頼しているのでしょうか?信頼している場合は、追加のヘッダーを使用して、クレデンシャル付きのリクエストを明示的に許可する必要があります。

fetch でクレデンシャルを送信するには、次のように credentials: "include" オプションを追加する必要があります

fetch('http://another.com', {
  credentials: "include"
});

これで、fetchanother.com から発信されたCookieを、そのサイトへのリクエストと共に送信します。

サーバーが *クレデンシャル付き* のリクエストを受け入れることに同意する場合、Access-Control-Allow-Origin に加えて、レスポンスに Access-Control-Allow-Credentials: true ヘッダーを追加する必要があります。

例えば

200 OK
Access-Control-Allow-Origin: https://javascriptinfo.dokyumento.jp
Access-Control-Allow-Credentials: true

注意点: クレデンシャル付きのリクエストの場合、Access-Control-Allow-Origin でスター * を使用することは禁止されています。上記のように、正確なオリジンを指定する必要があります。これは、サーバーがこのようなリクエストを行うことを信頼している相手を本当に知っていることを確認するための追加の安全対策です。

まとめ

ブラウザの観点からは、クロスオリジンリクエストには「安全な」リクエストとそれ以外の2種類があります。

「安全な」リクエストは、以下の条件を満たす必要があります

  • メソッド: GET、POST、または HEAD。
  • ヘッダー – 設定できるのは以下のみです
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Typeapplication/x-www-form-urlencodedmultipart/form-data、または text/plain に設定します。

重要な違いは、安全なリクエストは <form> タグまたは <script> タグを使用して古くから実行可能であったのに対し、安全でないリクエストはブラウザでは長期間不可能であったことです。

そのため、実際の違いは、安全なリクエストは Origin ヘッダー付きですぐに送信されるのに対し、他のリクエストの場合はブラウザが許可を求める予備的な「プリフライト」リクエストを行うことです。

安全なリクエストの場合

  • → ブラウザはオリジンと共に Origin ヘッダーを送信します。
  • ← クレデンシャルなしのリクエスト(デフォルトでは送信されない)の場合、サーバーは以下を設定する必要があります
    • Access-Control-Allow-Origin* または Origin と同じ値に設定します
  • ← クレデンシャル付きのリクエストの場合、サーバーは以下を設定する必要があります
    • Access-Control-Allow-OriginOrigin と同じ値に設定します
    • Access-Control-Allow-Credentialstrue に設定します

さらに、JavaScriptに Cache-ControlContent-LanguageContent-TypeExpiresLast-Modified、または Pragma 以外のレスポンスヘッダーへのアクセスを許可するには、サーバーは許可されたヘッダーを Access-Control-Expose-Headers ヘッダーにリストする必要があります。

安全でないリクエストの場合、リクエストされたリクエストの前に予備的な「プリフライト」リクエストが発行されます

  • → ブラウザは、以下のヘッダーを含む OPTIONS リクエストを同じURLに送信します
    • Access-Control-Request-Method にはリクエストされたメソッドが含まれます。
    • Access-Control-Request-Headers には、安全でないリクエストされたヘッダーがリストされます。
  • ← サーバーは、ステータス200と以下のヘッダーで応答する必要があります
    • Access-Control-Allow-Methods には許可されたメソッドのリストが含まれます。
    • Access-Control-Allow-Headers には許可されたヘッダーのリストが含まれます。
    • Access-Control-Max-Age には、許可をキャッシュする秒数が含まれます。
  • その後、実際のリクエストが送信され、前の「安全な」スキームが適用されます。

タスク

重要度: 5

ご存知のとおり、HTTPヘッダー Referer は、通常、ネットワークリクエストを開始したページのURLを含んでいます。

例えば、https://javascriptinfo.dokyumento.jp/some/url から http://google.com をフェッチする場合、ヘッダーは次のようになります

Accept: */*
Accept-Charset: utf-8
Accept-Encoding: gzip,deflate,sdch
Connection: keep-alive
Host: google.com
Origin: https://javascriptinfo.dokyumento.jp
Referer: https://javascriptinfo.dokyumento.jp/some/url

ご覧のとおり、RefererOrigin の両方が存在します。

質問

  1. Referer にはさらに多くの情報が含まれているのに、なぜ Origin が必要なのでしょうか?
  2. Referer または Origin が存在しない、または正しくないということはあり得るのでしょうか?

Origin が必要なのは、Referer が存在しない場合があるためです。例えば、HTTPSからHTTPページを fetch する場合(安全性の低いものから安全性の高いものにアクセスする場合)、Referer は存在しません。

コンテンツセキュリティポリシー は、Referer の送信を禁止する場合があります。

後述するように、fetch には Referer の送信を防ぎ、変更することさえできるオプションがあります(同じサイト内)。

仕様上、Referer はオプションのHTTPヘッダーです。

Referer は信頼できないため、Origin が考案されました。ブラウザは、クロスオリジンリクエストに対して正しい Origin を保証します。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善のための提案がある場合は、コメントする代わりに、GitHubのissue またはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 短いコードを挿入するには <code> タグを使用し、複数行の場合は <pre> タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepen…)を使用してください。