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 エラーを再現できます。

  1. 対象オブジェクトのキャッシュを削除する(CloudFront > ディストリビューション > キャッシュ削除)
  2. 対象オブジェクトに CloudFront 経由で no-cors リクエストする
  3. 対象オブジェクトに 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 リクエストを作成できます。

  • ブラウザ上で動作する Web アプリからリクエストする
  • HTTP クライアントから Origin ヘッダーを付与してリクエストする

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 からアクセスする場合)。

こういった理由で、今回はこの方法は採用を見送りました。