CORSの設定をしているはずなのにCloudFrontでCORSエラーが発生してしまう場合の対処法を調べてみた
CORSの設定をしているはずなのにエラーが発生してしまうという事象に出くわしました。
Access to XMLHttpRequest at 'https://example.com/xxx/yyy/zzz' from origin 'http://localhost:8000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
CloudFront および S3 には次のような設定をしていました。
- CloudFront のキャッシュポリシーには、マネージドポリシーの
ChachingOptimezed
をアタッチ - CloudFront のオリジンリクエストポリシーには、マネージドポリシーの
CORS-S3Origin
をアタッチ - CloudFront のレスポンスヘッダーポリシーには、マネージドポリシーの
CORS-and-SecurityHeadersPolicy
をアタッチ - S3 バケットのアクセス許可で CORS の設定を記述(内容は以下)
[ { "AllowedHeaders": [ "*" ], "AllowedMethods": [ "GET", "HEAD" ], "AllowedOrigins": [ "*" ], "ExposeHeaders": [], "MaxAgeSeconds": 3000 } ]
追記 1
検証を進めたところ、レスポンスヘッダーポリシーの CORS 設定の挙動の認識に誤りがあることがわかりました。
オーバーライド設定を OFF にする場合には、S3 バケットのアクセス許可で CORS 設定の欄を空にする必要がありました。「S3 からのレスポンスのヘッダーに項目がなかったら」ヘッダーを追加するものと思っていましたが、実際の挙動としては「S3 に CORS 設定がなかったら」ヘッダーを追加なようでした。
CloudFront のレスポンスヘッダーポリシーで、マネージドポリシーを利用して CORS 設定を行う場合には、S3 側では CORS 設定を空にする必要がありそうです。
解決策(結論)
初めに結論を書いておきます。
CloudFront のレスポンスヘッダーポリシーには、マネージドポリシーの CORS-and-SecurityHeadersPolicy
ではなく、自前で定義するカスタムポリシーを使うことにより、エラー発生を防ぐことができるようになりました。
ポイントは、Access-Control-Allow-Origin のルールでオリジンのオーバーライドを有効にすることです。マネージドポリシーの CORS-and-SecurityHeadersPolicy
ではオリジンのオーバーライドが無効になっています。
CORS エラーの再現方法
では次に、どのようなケースで初めに提示した CORS 設定でもエラーが発生してしまうのかを見ていきます。
次のような手順を踏むことで CORS エラーを再現できます。
- 対象オブジェクトのキャッシュを削除する(CloudFront > ディストリビューション > キャッシュ削除)
- 対象オブジェクトに CloudFront 経由で no-cors リクエストする
- 対象オブジェクトに CloudFront 経由で cors リクエストする
ポイントは、no-cors リクエスト時のレスポンスを CloudFront にキャッシュさせることです。このキャッシュデータに cors でアクセスを試みるとエラーが発生します。
cors と no-cors
cors リクエストと no-cors リクエストの違いは、リクエストヘッダーに Origin ヘッダー(Origin: <scheme>://<hostname>:<port>
)が含まれているか否かです。Origin ヘッダーが含まれていれば cors リクエストです。
※ 厳密には、Origin ヘッダーを含み、且つリクエスト先とリクエスト元の Origin が異なる場合に Cross Origin なリクエストとなります。
cors リクエストの送り方
ブラウザはリクエスト時に自動で Origin ヘッダーを付与する仕組みになっているので、Web アプリケーションで img タグや fetch API を通してリソースへアクセスを試みると CORS リクエストとなります。
もしくは、curl などの HTTP クライアントで Origin ヘッダーを明示的に付与することでも CORS リクエストを作成できます。
no-cors リクエストの送り方
リクエストヘッダーに Origin が含まれなければいいので、次のような方法だと no-cors リクエストになります。
- HTTP クライアントから Origin ヘッダーを付与せずにリクエストする
- ブラウザのアドレスバーに URL を直接入力してアクセスする
CORS エラーが発生する仕組み
S3 に対して no-cors リクエストが届くと、S3 はレスポンスに CORS ヘッダー(Access-Control-Allow-Origin
)を含まずにオブジェクトを返却します。CloudFront も、この CORS ヘッダーを含まないレスポンスをキャッシュします。
この状態で今度は cors リクエストが届くと、CloudFront は Access-Control-Allow-Origin
が無いレスポンスを返却します。すると、ブラウザは「CORS アクセスが許可されていないリソース」と判断し、データへのアクセスを中止します。
CORS-and-SecurityHeadersPolicy が問題?
ここで疑問となるのが、CloudFront のレスポンスヘッダーポリシーにアタッチしたはずの CORS-and-SecurityHeadersPolicy
です。
このポリシーは、S3 から返却されたレスポンスに Access-Control-Allow-Origin
ヘッダーが存在しない場合に、Access-Control-Allow-Origin: *
を付与し直してクライアントへ返却する設定になっています。
しかしながら、今回の再現手順を踏むと、どういう訳か Access-Control-Allow-Origin
が付与されないままレスポンスが返されてしまっていました。
カスタムポリシーを作成して解決する
レスポンスヘッダーポリシーは、デフォルトで準備されているマネージドポリシーの他に、自前でルールを定義して適用することもできます。
カスタムルールを作成するには次のように操作します。
[CloudFront] > [ポリシー] > [レスポンスヘッダー] > [カスタムポリシー] > [レスポンスヘッダーポリシーを作成]
内容は下記のとおりです。
詳細: 名前: My-CORS-and-SecurityHeadersPolicy クロスオリジンリソース共有(CORS): CORS を設定: ON Access-Control-Allow-Origin: All origins Access-Control-Allow-Headers: All headers Access-Control-Allow-Methods: Customize: - GET - HEAD Access-Control-Expose-Headers: なし Access-Control-Max-Age: 3000 Access-Cpntrol-Allow-Credentials: OFF オリジンのオーバーライド: ON セキュリティヘッダー: Strict-Transport-Security: 最大経過時間: 31536000 オリジンのオーバーライド: ON X-Content-Type-Options: オリジンのオーバーライド: ON X-Frame-Options: オリジン: SAMEORIGIN オリジンのオーバーライド: ON X-XSS-Protection: X-XSS-Protection: 有効 ブロック: ON オリジンのオーバーライド: ON Referrer-Policy: Referrer-Policy: strict-origin-when-cross-origin オリジンのオーバーライド: ON Content-Security-Policy: OFF
作成したら CloudFront にアタッチします。
[CloudFront] > [ディストリビューション] > [ビヘイビア] > [編集] > [レスポンスヘッダーポリシー] > [My-CORS-and-SecurityHeadersPolicy]
カスタムポリシーのアタッチが完了すると、先のエラー手順を再現しても、cors リクエスト時には Access-Control-Allow-Origin: *
ヘッダーが返却され、CORS エラーが発生しなくなりました。
もう一つの解決策(キャッシュポリシーを変更する)
今回は採用を見送りましたが、キャッシュポリシーを変更することでも CORS エラーの発生を防ぐことができます。
具体的には、キャッシュポリシーを ChachingOptimezed
から Managed-Elemental-MediaPackage
に変更するという方法です。
Managed-Elemental-MediaPackage
はキャッシュキーとして Origin ヘッダーを含む設定になっています。要するに、リクエスト元の Origin が異なればキャッシュにはヒットせず S3 へのリクエストが発生するということです。
この挙動のおかげで、Access-Control-Allow-Origin ヘッダーを含まない no-cors リクエスト(Origin: なし)の結果を CloudFront がキャッシュしたとしても、次に cors リクエストを送った場合にはこのキャッシュにはヒットせず、Access-Control-Allow-Origin ヘッダーを含む新たなレスポンスを取得することが可能になります。
ただし、この挙動だと複数ドメインからリソースを取得するようなケースでキャッシュヒット率が低くなってしまいます(localhost
, www.example.com
, admin.example.com
など複数 Origin からアクセスする場合)。
こういった理由で、今回はこの方法は採用を見送りました。