Web

【セキュリティー】CORS(Cross-Origin Resource Sharing)まとめ Part 1

今回はエンジニアなら知っておきたいセキュリティーのCORSについて徳丸本を使って学習したので、これについてまとめていきたいと思います。めちゃくちゃ分厚いですが、エンジニアなら一冊持っておいていい本だと思います。ぜひ買ってみてください!

同一オリジンポリシー

CORSの前に同一オリジンポリシーについてかんたんに触れておきます。同一オリジンポリシーでは異なるホスト間でJavaScriptアクセスをすることはできません。同一オリジンである条件は以下のとおりです。

  • URLのホストが一致している
  • スキームが一致している
  • ポート番号が一致している

しかし、これだと今だと当たり前に行われているAPIを叩いてデータを取得するということができず不便です。そのため、相手側の許可があれば同一オリジンでなくても通信できるCORSの規格が策定されました。

CORS

前述のとおり、WebアプリケーションにおいてJavaScriptの活用が進むと、同一オリジンポリシーの制限を超えて、サイト間でデータをやり取りしたいニーズが強くなってきました。そこで、オリジンを超えてデータをやりとりできる仕様として、Cross-Origin Resource Sharing(CORS)が策定されました。

それでは、実際にクライアントとサーバーの詳しいやりとりも含めて見ていきます。ここでは、XMLHttpRequestのケースを見ていくことにします。

シンプルなリクエストの場合

シンプルなリクエストの場合、異なるオリジンにHTTPリクエストを送ることが相手側の許可なしに可能です。シンプルなリクエストとは以下のようなリクエストを言います。

メソッドは以下のうちいずれか
  • GET
  • HEAD
  • POST

セットするリクエストヘッダは以下に限る
  • Accpet
  • Accept-Language
  • Content-Language
  • Content-Type
Content-Typeヘッダーは以下のいずれかであること
  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

これら条件を満たすリクエストは無条件に送信することができます。実際にリクエストを投げてみた結果を下に書きました。

GET http://api.example.net/33/33-002.php HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: */*
Accept-Language: ja,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Origin: http://example.jp
Connection: keep-alive
Referer: http://example.jp/
Host: api.example.net
HTTP/1.1 200 OK
Server: nginx/1.10.3
Date: Wed, 05 May 2021 21:43:38 GMT
Content-Type: application/json
Content-Length: 71
Connection: keep-alive
X-UA-Compatible: IE=edge

{"zipcode":"100-0100","address":"\u6771\u4eac\u90fd\u5927\u5cf6\u753a"}

ブラウザのコンソールを見ると次のようにエラーが表示されていて、ブラウザによりレスポンスの読み込みがブロックされています。

「クロスオリジン要求をブロックしました: 同一生成元ポリシーにより、http://api.example.net/33/33-002.php にあるリモートリソースの読み込みは拒否されます (理由: CORS ヘッダー ‘Access-Control-Allow-Origin’ が足りない)。」

エラー文にも書いているようにクロスオリジンからの読み出しを許可するためにはAccess-Control-Allow-Originヘッダーをセットする必要があります。このヘッダーがセットされているとブラウザはリソースを読み出すことができます。

シンプルなリクエストでない場合

シンプルなリクエストの条件を満たさない場合、ブラウザはプリフライトリクエストを送信します。では、リクエストのContent-Typeにapplication/jsonを指定してPOSTしてみます。

OPTIONS http://api.example.net/33/33-004.php HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: */*
Accept-Language: ja,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Referer: http://example.jp/
Origin: http://example.jp
Connection: keep-alive
Host: api.example.net
Content-Length: 0

ブラウザはOPTIONSメソッドを送信しています。これがプリフライトリクエストです。コンソールを見てみると、ブラウザで下のようなエラーがでます。

クロスオリジン要求をブロックしました: 同一生成元ポリシーにより、http://api.example.net/33/33-004.php にあるリモートリソースの読み込みは拒否されます (理由: CORS プリフライト応答からのヘッダー ‘Access-Control-Allow-Headers’ によりヘッダー ‘content-type’ が許可されていない)。

プリフライトリクエストを受け取ったサーバーはこれに応答する必要があり、応答がないと上のようなエラーが出るということになります。

要求の種類リクエストレスポンス
メソッドに対する許可Access-Control-Request-MethodAccess-Control-Allow-Methods
ヘッダに対する許可Access-Control-Request-HeadersAccess-Control-Allow-Headers
オリジンに対する許可OriginAccess-Control-Allow-Origin

では、正常に通信ができた場合のサーバーとのやりとりを見てみます。

OPTIONS http://api.example.net/33/33-004b.php HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: */*
Accept-Language: ja,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Referer: http://example.jp/
Origin: http://example.jp
Connection: keep-alive
Host: api.example.net
Content-Length: 0
HTTP/1.1 200 OK
Server: nginx/1.10.3
Date: Wed, 05 May 2021 22:21:17 GMT
Content-Type: text/plain;charset=UTF-8
Content-Length: 0
Connection: keep-alive
Access-Control-Allow-Origin: http://example.jp
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 10
X-UA-Compatible: IE=edge

プリフライトリクエストに対して、きちんと応答しているのが分かります。

POST http://api.example.net/33/33-004b.php HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: */*
Accept-Language: ja,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
content-type: application/json
Content-Length: 24
Origin: http://example.jp
Connection: keep-alive
Referer: http://example.jp/
Host: api.example.net

{"zipcode" : "100-0100"}
HTTP/1.1 200 OK
Server: nginx/1.10.3
Date: Wed, 05 May 2021 22:21:17 GMT
Content-Type: application/json
Content-Length: 71
Connection: keep-alive
Access-Control-Allow-Origin: http://example.jp
Access-Control-Max-Age: 10
X-UA-Compatible: IE=edge

サーバーから許可がでたので、POSTしています。成功しているのが分かりますね。

各ヘッダーについて簡単にだけ説明しておきます。

Access-Control-Allow-Headers (リンク)

Access-Control-Request-Headers を含むプリフライトリクエストへのレスポンスで、実際のリクエストの間に使用できる HTTP ヘッダーを示すために使用されます。

Access-Control-Expose-Headers (リンク)

レスポンスの一部としてどのヘッダーを公開するかを、その名前を列挙して示します。クライアントが他のヘッダーにアクセスできるようにするには、 Access-Control-Expose-Headers ヘッダーを使用してヘッダーを列挙する必要があります。

さいごに

特に最近のWebアプリケーション開発ではフロントエンドとバックエンドに分けて開発することが多いかと思うので、CORSは知っておいたほうがいいということで勉強してみました。少し内部について深く知れておもしろいと感じました。次は認証情報を含むリクエストについてまとめていきたいと思います。

ここまで読んでいただきありがとうございました。

ABOUT ME
sakai
東京在住の30歳。元々は車部品メーカーで働いていてましたが、プログラミングに興味を持ちスクールに通ってエンジニアになりました。 そこからベンチャー → メガベンチャー → 個人事業主になりました。 最近は生成 AI 関連の業務を中心にやっています。 ヒカルチャンネル(Youtube)とワンピースが大好きです!