CORS でつまづいた借りを返しにきた


名著「安全なアプリケーションの作り方」をめちゃくちゃ参考にしています
より詳細には MDN web docs の CORS などを御覧ください
※ 網羅性を担保できてない部分もありますのでご注意ください

筆者は S3 + CloudFront のフロントに API Gateway + Lambda のバックエンドという一般的な 静的な Webアプリを作成した際に Cross-Origin Resource Sharing(以下CORS) にハマりました。
その借りを返すべく調べてみました。

CORS の話に入る前に

Web アプリケーションのセキュリティを語る際に、当然ながら Web アプリそのもののセキュリティ対策は語られますが、実はブラウザも多くのセキュリティ対策を施してくれています。

ブラウザが実施する多くの対策のうちの一つが、クライアントスクリプト(Javascript など)の動作の制限です。
ブラウザは Javascript などのクライアントスクリプトの実行環境を制限することで以下のことができないようにしています

  • ローカルファイルへのアクセス禁止
  • プリンタなどの資源利用禁止
  • ネットワークの制限(同一オリジンポリシー)

実はブラウザの制限機構により、異なるオリジン(オリジンの定義に関しては後述。いまは同じ FQDN くらいのイメージで)以外への通信は、原則できないようになっているのです。
しかし、普段から Javascript で別のオリジンへのアクセス(ex. https://example-a.com から https://example-b.com) などは行っていますよね。ここに CORS の話が関わってきます。

同一オリジンポリシーとはなにか

ここで「同一オリジンポリシー」とはどんな制限なのでしょうか?

同一オリジンポリシーとは
「あるオリジンから読み込まれた文書やスクリプトについて、そのリソースから他のオリジンのリソースにアクセスできないようにする制限」
のことです。

ここで、同一オリジンとは以下をすべて満たすものを指します

1. FQDN が一致している
2. スキーム(プロトコル)が一致している(HTTP, HTTPS)
3. ポート番号が一致している

同一オリジンの判定の具体例はこちらを参考にしてください
正確にはドメイン名ではなく、FQDN が一致している必要があります。スキームが一致しているという部分などは、恥ずかしながら筆者も調べるまで知りませんでした。

クロスオリジンアクセスとはどういうことか

オリジンを https://example-a.com, ポート番号は皆 8080 と仮定したときに
同一オリジンか、クロスオリジンかは以下のようになります。
クロスオリジンと判定される場合は、上記の同一オリジンポリシーのため Javascript から原則通信できません。

URL 判定 理由
https://example-a.com/hoge/index.html 同一オリジン パス違いなだけ
http://example-a.com クロスオリジン プロトコルが異なる
https://hoge.example-a.com クロスオリジン FQDN(ホスト部分) が異なる
https://example-a.com クロスオリジン FQDN(ドメイン部分) が異なる

表よりも情報量は落ちますがイメージ図

本論からはそれますが、Javascript からではなければブラウザが許可しているクロスオリジンアクセスがあります。

クロスオリジンアクセスできる例外

詳細は「安全なWebアプリケーションの作り方」を参照してください。

  • frame, iframe 要素(Javascript での クロスオリジンのドキュメントにはアクセスできない)
  • img 要素の src 属性(リクエスト時に、画像のあるホストに対するクッキーがついてしまう。ステートフルなふるまいの可能性)
  • script 要素の src 属性(クロスオリジンの javascript ソース取得のリクエスト時に Javascript が置いてあるサイトに対するクッキーがついてしまう。ステートフルなふるまいの可能性)
  • CSS内の @import, HTML の Link 要素による CSS 取得, JS の addImport メソッドによる CSS 取得
  • form 要素の action 属性

CORS とはなにか

ここまでで「ブラウザのセキュリティ対策である同一オリジンポリシーによって、クロスオリジンアクセスは原則制限されている」ということがわかります。

しかし、現実的にはクロスオリジンで Web 上のリソースを活用したい場面はたくさんあります。
そこで作成された規格が「CORS : Cross-Origin Ressource Sharing」です。

CORSは、「同一オリジンポリシーだと他のオリジンを利用できないから、特定の条件を満たすときはクロスオリジンアクセスを許可する規格」です。
同一オリジンポリシーに依存するアプリケーションと整合性をもった上で、クロスオリジンアクセスできるように作成されています。

CORS はどどのような条件で有効になるのか

クロスオリジンアクセスが可能になるのは、特定の条件を満たすときです。この特定の条件とはどのようなものなのか見ていきます。

まず リクエスト によって CORS に関する挙動は異なります。

それは以下の 3 つの要件をすべて満たすリクエストか、そうでないリクエスト(1つでも満たさない要件がある)かです。
ここでは要件を満たすリクエストを「単純リクエスト」そうでないものを「それ以外のリクエスト」と呼びます。
より詳細な要件の定義はこちら

「単純リクエストの要件」
1. メソッドが GET, HEAD, POST のいずれか
2. リクエストヘッダが Accept, Accept-Language, Content-Language, Content-Type
3. Content-Type が application/x-www-form-urlencoded, multipart/formdata, text/plain のいずれか

単純リクエストの場合

単純リクエストの場合の挙動

  1. 【クライアント】ヘッダーに Origin: https://example-a.com を含めてGETリクエストを https://example-b.com へ送信する
  2. 【サーバー】レスポンスのヘッダーAccess-Control-Allow-Originの設定値とリクエストのOriginを検証して、条件を満たせば Status:200 とともにリクエストに対応したレスポンスを返す

例えば、サーバーの Access-Control-Allow-Origin:https://example-a.com と設定されている場合に、Origin: https://example-a.com のリクエストが来たら正常なレスポンスをクライアントに返却します。
Access-Control-Allow-Origin: * であればどのようなオリジンからのリクエストに対しても、正常なレスポンスを返却します。

必要なのはサーバー側で「オリジンに対する許可」を

「単純リクエストの場合、リクエストの Origin がレスポンスの Access-Control-Allow-Origin の条件を満たしていれば、CORS を使用したクロスオリジンアクセスが可能になります」

CORS を使用したクロスオリジンアクセスができる条件 リクエスト レスポンス
オリジンに対する許可 Origin Access-Control-Allow-Origin

単純ではないリクエストの場合

単純なリクエストの要件を 1 つでも満たさない場合は、クライアントとサーバーの間でやり取りが増えます。
単純なリクエストの要件ででてきた、メソッド、リクエストヘッダー、Content-Type についての検証が必要だからです。
Content-Type などの正確な取り扱いはこちらを参照してください

単純なリクエストでない場合の挙動

  1. 【クライアント】ヘッダーの Method を OPTIONS にセットした上で Origin: https://example-a.com, Access-Control-Request-Methods:POST(使用したいメソッド), Access-Control-Request-Headers:Content-Type(使用したいヘッダー)を含めて プリフライトリクエストhttps://example-b.com へ送信する
  2. 【サーバー】設定されているAccess-Control-Allow-Origin:*(許可するオリジン), Access-Control-Allow-Methods:POST,GET(許可するメソッド), Access-Control-Allow-Headers:X-PINGOTHER, Content-Type(許可するリクエストヘッダ)の設定値とリクエストのOriginを検証して、条件を満たせば正常なレスポンスを返す
  3. 【クライアント】1 にのヘッダーにリクエスト内容を含めて、POST(先程はOPTIONSだった)メソッドでリクエストを https://example-b.com へ送信する
  4. 【サーバー】レスポンスのヘッダーAccess-Control-Allow-Originの設定値とリクエストのOriginを検証して、条件を満たせば Status:200 とともにリクエストに対応したレスポンスを返す

実際にリクエストを送る前に、プリフライトリクエストでリクエストをしても安全かどうか確認していることがわかります。

「単純ではないリクエストの場合、プリフライトリクエストで条件を満たしていれば、CORS を使用したクロスオリジンアクセスが可能になります」

CORS を使用したクロスオリジンアクセスができる条件 リクエスト レスポンス
オリジンに対する許可 Origin Access-Control-Allow-Origin
メソッドに対する許可 Access-Control-Request-Methods Access-Control-Allow-Methods
ヘッダに対する許可 Access-Control-Request-Headers Access-Control-Allow-Headers

クロスオリジンアクセス にまつわる注意点

認証がからむとき

クロスオリジンアクセスではリクエストヘッダがデフォルトでは送信されません。
クッキーなどでリクエストヘッダを利用して認証している際は、クロスオリジンアクセスすると認証されません。
以下のような設定が必要になります

- リクエスト : withCrendential:true
- レスポンス : Access-Control-Allow-Credentials:tue

プリフライトリクエストした際にリダイレクトがおこるとき

プリフライトリクエストした際に、リクエスト先のサーバーがプリフライトリクエストをリダイレクトすると、ブラウザの仕様上正しく動作しないことがあります。
対応としては主に以下の 3 つです

1. プリフライトが起こらないような、単純リクエストにする
2. リダイレクトが起こらないようにする
3. リダイレクト先に直接リクエストする

やはり詳細はここなどをご覧ください

リクエストに Origin が設定されないとき

リクエストに Origin が設定されないリクエストも存在するので注意が必要です。(通常の同一オリジン間での通信の場合は、ヘッダーに Origin が設定されません)
Fetch リクエストを HEAD または GET で行った場合には設定されないとのことです(ブラウザによる)

キャッシュとクロスオリジンアクセス

CDN 等で

  1. まず初回が同一オリジンからのアクセスだった場合、レスポンスにAccess-Control-Allow-Origin が含まれずにキャッシュ
  2. キャッシュされた後にクロスオリジンアクセスをした場合に、Access-Control-Allow-Originないキャッシュされたレスポンスのために正常に動作しない

ということが起こりえます。もう各 CDN 対応方法があるのでこのあたりを意識して設定しましょう
昔の記事ですがClassmethodさんのこちらの記事を参考にしました
キャッシュに関連してヘッダーのVaryこちらを参照

Access-Control-Allow-Origin に null はだめ

file: などのスキームに対しては null になるように定義されているため、null はやめる

クロスオリジンアクセス周りで問題がおきたら

クロスオリジンアクセスが絡みそうなことで問題が起きたら以下の観点をみてみると良いかもしれません。

  • リクエスト
    • そもそもクロスオリジンアクセスなのか
    • Origin が設定されているか
    • 単純リクエストなのか、そうでないリクエストなのか
    • リダイレクトされていないか
    • 認証が必要なリクエストで足りないヘッダがないか
  • レスポンス
    • キャッシュを見に行ってないか
    • 単純リクエスト、そうでないリクエストそれぞれで適切なヘッダーを返せているか

雑感

  • 単にドメイン名ではなく、オリジン(FQDN・プロトコル・ポート番号)が一致しているかどうかを判定しているので、「クロスドメインアクセス」などは CORS を語る文脈ではミスリードなのではと思いました

参考