Closed30

CORSって何

Yug (やぐ)Yug (やぐ)

Honoでapi立ててそこにReactからfetchするという単純なコードでCORSエラーが出て詰まった
この際にちゃんとCORSの概念と対処法を理解しておきたい

知りたいのは

  • なんのために存在するのか
    • エラーになるの面倒だが意義があるから存在しているはず
  • "CORS"とは概念なのかエラーなのか
Yug (やぐ)Yug (やぐ)

なるほど
CORSは「別のオリジンに対してアクセス権を与えてあげるようブラウザに指示するための仕組み」か

オリジン間リソース共有 (Cross-Origin Resource Sharing, CORS) は、追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組みです。ウェブアプリケーションは、自分とは異なるオリジン (ドメイン、プロトコル、ポート番号) にあるリソースをリクエストするとき、オリジン間 HTTP リクエストを実行します。

オリジン間リクエストとは、例えば https://domain-a.com で提供されているウェブアプリケーションのフロントエンド JavaScript コードが fetch() を使用して https://domain-b.com/data.json へリクエストを行うような場合です。

Yug (やぐ)Yug (やぐ)

そもそもオリジンとは何か
https://developer.mozilla.org/ja/docs/Glossary/Origin

なるほど

ウェブコンテンツのオリジン (Origin) は、ウェブコンテンツにアクセスするために使われる URL の スキーム (プロトコル)、 ホスト (ドメイン)、 ポート番号 によって定義されます。スキーム、ホスト、ポート番号がすべて一致した場合のみ、 2 つのオブジェクトは同じオリジンであると言えます。

オリジンは3つの構成要素を持つ(これ全部一致して初めてその2つは「同じオリジンである」と言える)

  • スキーム(プロトコル)
  • ホスト(ドメイン)
  • ポート番号

http://localhost:5000/http://localhost:5001/ではポート番号が違うからオリジンが違うということになり、CORSエラーが発生したということか、なるほど~~~

操作によっては同じオリジンのコンテンツに限定されており、この制約は CORS を使用して緩和することができます。

例の部分がすっごいわかりやすい
https://developer.mozilla.org/ja/docs/Glossary/Origin#例

Yug (やぐ)Yug (やぐ)

"オリジン間HTTPリクエスト"と呼ぶのか

ウェブアプリケーションは、自分とは異なるオリジン (ドメイン、プロトコル、ポート番号) にあるリソースをリクエストするとき、オリジン間 HTTP リクエストを実行します。

オリジン間リクエストとは、例えば https://domain-a.com で提供されているウェブアプリケーションのフロントエンド JavaScript コードが fetch() を使用して https://domain-b.com/data.json へリクエストを行うような場合です。

Yug (やぐ)Yug (やぐ)

あーやっぱ「セキュリティ上の理由」がCORSエラーの理由であり意義なんだなぁ

セキュリティ上の理由から、ブラウザーは、スクリプトによって開始されるオリジン間 HTTP リクエストを制限しています。例えば、fetch() や XMLHttpRequest は同一オリジンポリシー (same-origin policy) に従います。つまり、これらの API を使用するウェブアプリケーションは、そのアプリケーションが読み込まれたのと同じオリジンに対してのみリソースのリクエストを行うことができ、それ以外のオリジンからの場合は正しい CORS ヘッダーを含んでいることが必要です。

fetch()などのAPIはデフォルトで同一オリジンポリシーに従うらしい

Yug (やぐ)Yug (やぐ)

あーそういうことか、なるほど~~~

これにより、悪意のある可能性のあるドキュメントを隔離し、起こりうる攻撃のベクターを減らすことができます。例えば、インターネット上の悪意のあるウェブサイトがブラウザー内で JS を実行して、 (ユーザーがサインインしている) サードパーティのウェブメールサービスや (公開 IP アドレスを持たないことで攻撃者の直接アクセスから保護されている) 企業のイントラネットからデータを読み取り、そのデータを攻撃者に中継することを防ぎます。

JSはブラウザで実行されちゃうからセッションの認証情報とか盗み見られたりしたらやばいという前提がまずあって、そういう不正リクエストは現在のオリジンからではなく他のオリジンから来るのは当然で、だからそれはデフォルトで排除しておいて、必要に応じて許可する形式にすれば良い、という流れか!!理解

Yug (やぐ)Yug (やぐ)

なんか日本語おかしいような

最近のブラウザーは CORS を fetch() や XMLHttpRequest などの API で使用して、オリジン間 HTTP リクエストのリスクの緩和に役立てています。

CORSというのは前述の通り「許可してあげる仕組み」なので、この文脈だとfetch()とかが使ってるのはCORSというより"同一オリジンポリシー"では?

「CORS」と「同一オリジンポリシー」は対の位置にある概念な気がする
「許可してあげる仕組み」と「デフォルトでは許可しない仕組み」なので

まぁCORSを有効化することができるようになってるよ、ということなのかな

Yug (やぐ)Yug (やぐ)

実態はHTTPヘッダーか

オリジン間リソース共有の仕様は、ウェブブラウザーから情報を読み取ることを許可されているオリジンをサーバーが記述することができる、新たな HTTP ヘッダーを追加することで作用します。

プリフライト...ほう
事前に認証しちゃって、okの場合のみリクエスト受け付けるみたいなことができるってことかな?凄そう

加えて仕様書では、サーバーの情報に副作用を引き起こすことがある HTTP のリクエストメソッド (特に GET 以外の HTTP メソッドや、特定の MIME タイプを伴う POST) のために、ブラウザーが HTTP の OPTIONS リクエストメソッドを用いて、あらかじめリクエストの「プリフライト」 (サーバーから対応するメソッドの一覧を収集すること) を行い、サーバーの「認可」のもとに実際のリクエストを送信することを指示しています。サーバーはリクエスト時に「資格情報」 (Cookie や HTTP 認証など) を送信するべきかをクライアントに伝えることもできます。

Yug (やぐ)Yug (やぐ)

JSではわからないけどコンソール見れば詳細まで表示されるようになってるのか

CORS は様々なエラーで失敗することがありますが、セキュリティ上の理由から、エラーについて JavaScript から知ることができないよう定められています。コードからはエラーが発生したということしか分かりません。何が悪かったのかを具体的に知ることができる唯一の方法は、ブラウザーのコンソールで詳細を見ることです。

Yug (やぐ)Yug (やぐ)

絶対プリフライト起きる訳ではなさそう

リクエストによっては CORS プリフライトを発生させません。これをこの記事では古い CORS 仕様書に倣って単純リクエストと呼んでいますが、 (現在 CORS を定義している) Fetch 仕様書 ではこの用語を使用していません。

Yug (やぐ)Yug (やぐ)

あーそういうことか、プリフライトはリクエスト側が善意でやるやつってことか

単純リクエストとは異なり、「プリフライト」リクエストは始めに OPTIONS メソッドによる HTTP リクエストを他のドメインにあるリソースに向けて送り、実際のリクエストを送信しても安全かどうかを確かめます。オリジン間リクエストがユーザーデータに影響を与える可能性があるような場合に、プリフライトを行います。

じゃあサーバーによるpreLoadの事前認証みたいなことではないな

Yug (やぐ)Yug (やぐ)

ん?プリフライトって明示的に善意で書くみたいなことではないっぽい

プリフライトが行われるリクエストの例です。

const fetchPromise = fetch("https://bar.other/doc", {
  method: "POST",
  mode: "cors",
  headers: {
    "Content-Type": "text/xml",
    "X-PINGOTHER": "pingpong",
  },
  body: "<person><name>Arun</name></person>",
});

fetchPromise.then((response) => {
  console.log(response.status);
});

上記の例では、 POST で送信する XML の本体を作成しています。また、標準外の X-PINGOTHER HTTP リクエストヘッダーを設定しています。このようなヘッダーは HTTP/1.1 プロトコルに含まれていませんが、ウェブアプリケーションでは一般的に便利なものです。リクエストで Content-Type に text/xml を使用しており、かつ独自のヘッダーを設定しているため、このリクエストではプリフライトを行います。

じゃあ標準外のリクエストヘッダーだったりしたら自動で裏でプリフライトが走るようになってるってことかな?であれば良さげ

Yug (やぐ)Yug (やぐ)

プリフライトリクエストを表すOPTIONSメソッドなるものがあるらしい

最初のブロックは、OPTIONS メソッドによるプリフライトリクエストを表します。ブラウザーは上記の JavaScript コードで使用していていたリクエストの引数に基づいて、プリフライトの送信が必要であることを判断します。これによりサーバーは実際のリクエストの引数によって送られるリクエストを受け入れ可能かを応答することができます。 OPTIONS は HTTP/1.1 のメソッドであり、サーバーから付加的な情報を得るために用いるもので、また安全なメソッド、つまりリソースを変更するためには使用できないメソッドです。

Yug (やぐ)Yug (やぐ)

こちらも参考になる
https://aws.amazon.com/jp/what-is/cross-origin-resource-sharing/

インターネット技術がまだ新しかった時代では、クロスサイトリクエストフォージェリ (CSRF) の問題がありました。これらの問題では、被害者のブラウザから別のアプリケーションに偽のクライアントリクエストが送信されていました。

たとえば、被害者が銀行のアプリケーションにログインしたとします。その後、だまされて新しいブラウザタブに外部ウェブサイトを読み込まされたとします。そして、その外部ウェブサイトは被害者のクッキー認証情報を使用して、被害者になりすましてデータを銀行のアプリケーションに中継するのです。結果として、権限のないユーザーが銀行のアプリケーションに意図せずアクセスするようになったのです。

このような CSRF 問題を防ぐことを目的として、すべてのブラウザが同一オリジンポリシーを実装するようになりました。

同一オリジンのポリシーは非常に安全ですが、実際のユースケースでは柔軟性がありません。

クロスオリジンリソース共有 (CORS) は同一オリジンポリシーの拡張です。これは、外部の第三者とリソースを正式に共有するために必要です。たとえば、公開または承認されている外部 API からデータを取得する場合は、CORS が必要です。また、権限のある第三者に自分のサーバーリソースへのアクセスを許可したい場合も、CORS が必要とされています。

ただし、一部の HTTP リクエストは複雑と見なされ、実際のリクエストを送信する前にサーバーの確認が必要です。事前承認プロセスはプリフライトリクエストと呼ばれます。

Yug (やぐ)Yug (やぐ)

自分のエラーを一応貼っとく

2番目のやつに関しては、200 ok なのにnet: ERR FAILEDと両立してるのはなんか不思議
リクエストが成功してレスポンスは成功裏に返却されてるけどそれを受け取る権限がないためCORS特有エラーが出てるということなのかな
https://developer.mozilla.org/ja/docs/Web/HTTP/Status/200


エンドポイント名変えたら200 ok出なくなった、謎だ

Yug (やぐ)Yug (やぐ)

ほー

サーバーサイドでは、以下のレスポンスヘッダーを追加する必要があります。

Access-Control-Allow-Origin: https://trusted-one.co.jp  // 特定のサイトを許可する
Access-Control-Allow-Origin: *   // 全てのサイトを許可する(危険なのでプロダクトでは基本的には使わない)
Access-Control-Allow-Headers "X-Requested-With, Origin, X-Csrftoken, Content-Type, Accept"  // この辺は使うフレームワークにより異なるが許可するヘッダーを定義しておく。

originやMethodsは簡単だけどHeadersが難しそうだ、知らん単語だらけなので

Yug (やぐ)Yug (やぐ)

なるほど、正規表現使ったりenvファイルに分離したりするのか

実際にプロダクトで設定するときは、脆弱性を考慮し特定のオリジンからのリクエストのみ受け付けるようにします。

 // OK: https://front-end.com or https://abc.front-end.com...
  // NG: https://evilsite.com...
  const origin = req.headers.origin;
  if (
    origin === process.env.FRONTEND_ORIGIN  // https://front-end.com
    || /^https:\/\/.+\.front-end\.com$/.test(origin)
  ) {
    res.header('Access-Control-Allow-Origin', origin);
    res.header('Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept');
    res.header('Access-Control-Allow-Methods', 'PUT, DELETE, OPTIONS');
    ...
  }
...
Yug (やぐ)Yug (やぐ)

ふむ

単純リクエストとは異なり、プリフライトリクエスト (Preflight Request) は、リクエストの始めに OPTIONS メソッドで対象の異なるオリジンにリクエストを送り、実際のリクエストを送っても問題ないか確認します。
該当するリクエストは以下になります。

  • PUT
  • DELETE
  • CONNECT
  • OPTIONS
  • TRACE
  • PATCH

プリフライトリクエストのレスポンスとして、アクセスを許可するメソッドをレスポンスヘッダーに含める必要があります。

Access-Control-Allow-Methods: PUT, DELETE, PATCH

これらのメソッドは自動的にプリフライトリクエストとなるということかな
んでそのレスポンスにちゃんとアクセス許可するメソッドをヘッダーとして付与する必要があると

Yug (やぐ)Yug (やぐ)

ほー、cookieもsame origin policy(SOP)適用されるのか

a.com という js のページを開いた状態で b.com へ XMLHttpRequest を送る際に b.com の Cookie も含めてリクエストを送りたいという場合、デフォルトでは異なる Origin に対して Cookie は送信されません。 (自分はこれにハマってしまったのでお気をつけて... 😖 )

Origin をまたいだ XMLHttpRequest で Cookie を送りたい場合、
Cookie の送受信を許可するために、クライアントサイド・サーバーサイドに実装が必要になります。

でもJSによる不正行為を防止するのがsame origin policyの意義であって、cookieなんかただの文字列だからJSではないのでなぜわざわざ規制するのかわからん

Yug (やぐ)Yug (やぐ)

これは確かにやる必要がありそうだ

same-origin (既定値): 同一オリジンのリクエストに対してのみ資格情報を送信し、含めます。

fetch('https://trusted-api.co.jp', {
 mode: 'cors',
 credentials: 'include' // ここを追加。
});

規定値はincludeではなくsame-originになってるので

same-origin (既定値): 同一オリジンのリクエストに対してのみ資格情報を送信し、含めます。

https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetch#資格情報を含める

Yug (やぐ)Yug (やぐ)

へぇーワイルドカードでもダメな場合があるのか

credentials mode (withCredentials パラメータを着けている場合) では Access-Control-Allow-Origin は *(ワイルドカード) だとダメとのこと。

なので、このように Origin を明示的に指定する必要があります。

Yug (やぐ)Yug (やぐ)

なるほど~~~すごく勉強になる

これだけ色々書いておいて、クライアントサイドもサーバーサイドも実装したし大丈夫!と意気込んでテストを行った結果、以下のようなエラーに遭遇しました...
API の CORS 許可設定はしっかりしていたはずなのに...

Access to XMLHttpRequest at 'https://authentication.com/auth?client_id=12345&scope=openid&profile&email&response_type=code&redirect_uri=http://api.com/auth/cb&state=abcdefg'
(redirected from 'http://api.com') from origin 'http://front-end.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

エラーの内容をよーく見てみると...

(redirected from 'http://api.com') from origin 'http://front-end.com' has been blocked by CORS policy

これですね... API の CORS の許可設定していたので問題ないかと思っていたのですが、

ブラウザが最初にアクセスする URL は Front End (front-end.com) であり、API でリダイレクトされて認証サーバにリクエストが飛ばされた結果、認証サーバでは Front End (front-end.com) の Origin の許可設定がされていないためエラーを返されてしまったわけです...。

たとえ API に CORS の許可設定を入れていたとしても、リダイレクトされてしまうとその先のサーバが CORS の許可設定を入れていないとエラーになるという観点が抜け漏れていました... :sob:

  • 教訓

CORS エラーに遭遇した時は冷静にリクエストの流れを辿り、どのサーバーで CORS の許可設定がされていないのかを確認するようにしましょう。エラー内容から読み解くことができるはずです。

この記事でもリダイレクトで困ったとか言ってたな
https://qiita.com/piro877/items/427a345346f7209a530e

Yug (やぐ)Yug (やぐ)

へー、エラーにはならないだけで帰ってくるのは空のレスポンスなのか。使いどころが気になる

'no-cors': クロスオリジンリソース共有ができない場合に、エラーとはならず空のレスポンスが返却される。

このスクラップは2025/03/01にクローズされました