CORS の仕組みとハマりどころ - プリフライト・credentials・Allow-Origin を理解する

CORS の仕組みとハマりどころ - プリフライト・credentials・Allow-Origin を理解する

作成日:
読了:10
更新日:

フロントエンドから別ドメインの API を叩いて「No 'Access-Control-Allow-Origin' header is present」に阻まれた経験は、誰しもあると思います。CORS(Cross-Origin Resource Sharing / オリジン間リソース共有)は、仕組みを知らないと延々ハマり、知っていれば数分で解決できる領域です。

この記事では、CORS の仕組みと典型的なハマりどころを、MDN と WHATWG Fetch Standard を一次ソースに整理します。

まず: オリジンと同一オリジンポリシー

オリジンは「スキーム + ホスト + ポート」の3点セットです。3つすべてが一致して初めて「同一オリジン」になります(パスの違いは関係なし)。

基準 http://example.com/dir/a.html との比較判定
http://example.com/dir2/b.html同一(パスだけ違う)
https://example.com/a.html別オリジン(スキーム違い)
http://example.com:81/a.html別オリジン(ポート違い)
http://news.example.com/a.html別オリジン(ホスト違い)

同一オリジンポリシーは、あるオリジンのスクリプトが別オリジンのリソースを勝手に読み取れないようにするブラウザの安全機構です。fetchXMLHttpRequest はこれに従うため、適切な CORS ヘッダがない限りクロスオリジンのレスポンスを JS から読めません。CORS は「サーバが HTTP ヘッダで、どのオリジンからの読み取りを許すかブラウザに伝える」仕組みです。

単純リクエストとプリフライト

CORS には2パターンあります。

単純リクエスト(preflight が飛ばない)

次をすべて満たすと「単純リクエスト」となり、事前確認なしで本リクエストが飛びます。

  • メソッドが GET / HEAD / POST のいずれか
  • 手動で付けるヘッダが安全リスト(Accept / Accept-Language / Content-Language / Content-Type / Range)のみ
  • Content-Typeapplication/x-www-form-urlencoded / multipart/form-data / text/plain のいずれか

プリフライト(OPTIONS で事前確認)

上記を1つでも外れると、ブラウザは本リクエストの前に OPTIONS メソッドで事前確認(preflight)を送ります。代表的なトリガーは:

  • PUT / DELETE / PATCH などのメソッド
  • POST でも Content-Type: application/json
  • 独自ヘッダ(例 X-Custom-Header)を付ける場合

プリフライトでは Access-Control-Request-MethodAccess-Control-Request-Headers で「これから何をするか」をサーバに伝えます。

主なヘッダ

サーバが返すレスポンスヘッダ:

ヘッダ役割
Access-Control-Allow-Origin許可するオリジン(具体的に1つ、または *
Access-Control-Allow-Methods許可するメソッド(preflight 応答)
Access-Control-Allow-Headers許可するリクエストヘッダ(preflight 応答)
Access-Control-Allow-Credentialstrue で credentials 付きを許可
Access-Control-Expose-HeadersJS から読めるようにするレスポンスヘッダ
Access-Control-Max-Agepreflight 結果をキャッシュする秒数

実際の preflight のやりとりはこうなります(MDN の例)。

プリフライト(OPTIONS)
OPTIONS /doc HTTP/1.1
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,x-pingother
サーバの応答 → これで本リクエストに進める
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400

Access-Control-Max-Age で preflight をキャッシュすると、毎回 OPTIONS が飛ぶのを抑えられます(上限はブラウザ依存。Chromium は2時間、Firefox は24時間)。

credentials(Cookie・認証情報)付きの注意

デフォルトではクロスオリジンに Cookie 等は送られません。送るには fetch(url, { credentials: 'include' }) を使いますが、サーバ側に追加の条件が要ります。

  • Access-Control-Allow-Origin* は使えない。具体的なオリジンを返す必要がある
  • Access-Control-Allow-Credentials: true が必須
  • credentials 付きでは Allow-Methods / Allow-Headers* も「文字どおりのアスタリスク」として扱われるので、明示列挙が必要
credentials 付きの応答
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Credentials: true
Vary: Origin

NOTE

Authorization ヘッダは特別扱いで、Access-Control-Allow-Headers: *(ワイルドカード)では許可されません。明示的に Authorization と列挙する必要があります(JWT を Bearer トークンで送るときに踏みがち)。

よくあるハマりどころ

  • 「No 'Access-Control-Allow-Origin' header」: サーバが CORS ヘッダを返していない。サーバを直せるならヘッダを追加。直せないなら自分で管理するプロキシ経由にする
  • 「credentials is not supported if Allow-Origin is '*'」: credentials 付きなのに * を返している。具体的オリジン + Allow-Credentials: true に直す
  • Vary: Origin の付け忘れ: オリジンごとに Access-Control-Allow-Origin を出し分けるなら Vary: Origin 必須。忘れると、あるオリジン向けの応答がキャッシュされて別オリジンに返り、CORS エラーやキャッシュ汚染が起きる
  • preflight 未対応: サーバが OPTIONS に正しく応答しないと本リクエストへ進めない

いちばん大事な勘所: CORS は認可ではない

最後に、最も誤解されやすい点です。CORS はブラウザの仕組みであり、サーバのアクセス制御(認証・認可)ではありません。

  • CORS ヘッダは「ブラウザが、クロスオリジンのレスポンスを JS に渡してよいか」を制御するだけ
  • サーバ間通信(server-to-server)や curl は CORS を完全に無視できる
  • したがって機密データの保護を CORS に頼ってはいけない。保護はサーバ側の認証・認可(と適切な HTTP ステータス)で行う

「CORS を緩めたらデータが漏れる」のではなく、「CORS はそもそもデータ保護の仕組みではない」と理解するのが正解です。

まとめ

  • オリジン=スキーム+ホスト+ポート。同一オリジンポリシーが土台で、CORS はサーバが「読み取り許可」をヘッダで伝える仕組み
  • 単純リクエスト(GET/HEAD/POST + 安全なヘッダ/Content-Type)以外は preflight(OPTIONS)が飛ぶ
  • credentials 付きは * 不可。具体的オリジン + Allow-Credentials: trueAuthorization は明示列挙
  • オリジンを出し分けるなら Vary: OriginAccess-Control-Max-Age で preflight をキャッシュ
  • CORS は認可ではない。機密保護はサーバ側の認証・認可で

エラーメッセージの一語一句には意味があります。「どのヘッダが足りないのか」を読めるようになれば、CORS はもう怖くありません。

参考リンク

Cookie・セッション・SameSite の基礎 - Secure / HttpOnly と CSRF・CORS の関係

Cookie・セッション・SameSite の基礎 - Secure / HttpOnly と CSRF・CORS の関係

11

Web認証の土台となる Cookie・セッション・SameSite を実務目線で整理します。Set-Cookie の属性(Expires/Max-Age・Domain/Path)、Secure と HttpOnly が防ぐ脅威、SameSite の Strict/Lax/None の違いと None に Secure が要る理由、サーバー側セッションとログイン後の ID 再生成、SameSite だけで CSRF を防ぎきれない理由、クロスオリジンで Cookie を送る CORS の条件、__Host-/__Secure- プレフィックスまで、MDN・RFC 6265bis・OWASP を一次ソースにまとめます。

REST API設計の基礎と冪等性 - HTTPメソッドの意味と Idempotency-Key

REST API設計の基礎と冪等性 - HTTPメソッドの意味と Idempotency-Key

10

REST API設計の基礎を、HTTPメソッドの意味(safe / idempotent)から整理します。リソース指向のURL設計、GET/POST/PUT/PATCH/DELETE の使い分けと冪等性の早見表、なぜ冪等性がリトライや二重課金防止に重要か、POST を安全にリトライする Idempotency-Key の仕組み、ステータスコードの使い分け、RFC 9457 のエラー形式まで、RFC 9110・MDN・Stripe を一次ソースにまとめます。