名著「安全なアプリケーションの作り方」をめちゃくちゃ参考にしています
より詳細には 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 のいずれか
単純リクエストの場合
単純リクエストの場合の挙動
- 【クライアント】ヘッダーに
Origin: https://example-a.com
を含めてGET
リクエストを https://example-b.com へ送信する - 【サーバー】レスポンスのヘッダー
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 などの正確な取り扱いはこちらを参照してください
単純なリクエストでない場合の挙動
- 【クライアント】ヘッダーの Method を
OPTIONS
にセットした上でOrigin: https://example-a.com
,Access-Control-Request-Methods:POST(使用したいメソッド)
,Access-Control-Request-Headers:Content-Type(使用したいヘッダー)
を含めて プリフライトリクエスト を https://example-b.com へ送信する - 【サーバー】設定されている
Access-Control-Allow-Origin:*(許可するオリジン)
,Access-Control-Allow-Methods:POST,GET(許可するメソッド)
,Access-Control-Allow-Headers:X-PINGOTHER, Content-Type(許可するリクエストヘッダ)
の設定値とリクエストのOrigin
を検証して、条件を満たせば正常なレスポンスを返す - 【クライアント】1 にのヘッダーにリクエスト内容を含めて、
POST(先程はOPTIONSだった)
メソッドでリクエストを https://example-b.com へ送信する - 【サーバー】レスポンスのヘッダー
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 等で
- まず初回が同一オリジンからのアクセスだった場合、レスポンスに
Access-Control-Allow-Origin
が含まれずにキャッシュ - キャッシュされた後にクロスオリジンアクセスをした場合に、
Access-Control-Allow-Origin
ないキャッシュされたレスポンスのために正常に動作しない
ということが起こりえます。もう各 CDN 対応方法があるのでこのあたりを意識して設定しましょう
昔の記事ですがClassmethodさんのこちらの記事を参考にしました
キャッシュに関連してヘッダーのVary
はこちらを参照
Access-Control-Allow-Origin に null はだめ
file:
などのスキームに対しては null になるように定義されているため、null はやめる
クロスオリジンアクセス周りで問題がおきたら
クロスオリジンアクセスが絡みそうなことで問題が起きたら以下の観点をみてみると良いかもしれません。
- リクエスト
- そもそもクロスオリジンアクセスなのか
- Origin が設定されているか
- 単純リクエストなのか、そうでないリクエストなのか
- リダイレクトされていないか
- 認証が必要なリクエストで足りないヘッダがないか
- レスポンス
- キャッシュを見に行ってないか
- 単純リクエスト、そうでないリクエストそれぞれで適切なヘッダーを返せているか
雑感
- 単にドメイン名ではなく、オリジン(FQDN・プロトコル・ポート番号)が一致しているかどうかを判定しているので、「クロスドメインアクセス」などは CORS を語る文脈ではミスリードなのではと思いました