plan-4ging

2026/03/31

CORS

同一オリジンポリシー(Same-Origin Policy)

ブラウザが持つセキュリティの仕組みで、「あるオリジンから読み込まれた Web ページが、別のオリジンのリソースに JavaScript からアクセスすることを制限する」ポリシー

オリジンとは以下の3要素の組み合わせ
1つでも異なれば「別オリジン(クロスオリジン)」になる

origin = scheme + host + port

https://app.example.com:443/path
  ↑         ↑            ↑
scheme    host          port

【Same Origin】
https://example.com
- https://example.com/page-a # 同一(path のみ異なる)
- https://example.com/page-b # 同一(path のみ異なる)

【Cross Origin】
https://example.com
- http://example.com         # scheme が異なる
- https://api.example.com    # sub domain が異なる
- https://example.com:8080   # port が異なる

なぜ同一オリジンポリシーが必要か

下記のような事象を防ぐため

① ユーザーが A にアクセスする
② A の JavaScript が裏で全く異なるサービス B (クロスオリジン)にリクエスト
③ ユーザーのブラウザには B のセッション Cookie がある
④ B は正規ユーザーのリクエストとして処理してしまう
 → 勝手に処理(送金など)される

CORS(Cross-Origin Resource Sharing)とは

同一オリジンポリシーの制限をサーバー側が明示的に許可することで安全にクロスオリジンアクセスを可能にする仕組み
「コルス」と呼ばれる

app.example.com(フロントエンド)
 ↓ fetch('https://api.example.com/users')
api.example.com(バックエンド)
 ↓ レスポンスヘッダーに CORS 許可を付与
 Access-Control-Allow-Origin: https://app.example.com
 ↓
ブラウザが許可を確認し、レスポンスをフロントに渡す

原因・解決方法

エラー発生ケース

Access-Control-Allow-Originヘッダー不足/不一致

サーバーが CORS を許可していない

プリフライトリクエスト(Preflight Request、OPTIONS)失敗

サーバーがOPTIONSメソッドを処理していない

credentials 向けの設定不足

Cookie・Authorization を含む場合、特別な設定が必要

許可外のヘッダー・メソッド使用

カスタムヘッダーやPUT/DELETEが許可されていない

解決方法(基本)

レスポンスヘッダー設定を追加・見直し

Access-Control-Allow-Origin: https://app.example.com
 → アクセスを許可するオリジンを指定する
 → * を指定すると全オリジンを許可(credentials 使用時は * 不可)

Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
 → 許可する HTTP メソッドを指定する

Access-Control-Allow-Headers: Content-Type, Authorization, X-Custom-Header
 → 許可するリクエストヘッダーを指定する

Access-Control-Allow-Credentials: true
 → Cookie・認証情報を含むリクエストを許可する

Access-Control-Max-Age: 86400
 → プリフライトリクエストの結果をキャッシュする秒数
 → 設定することで OPTIONS リクエストの回数を減らせる

Access-Control-Expose-Headers: X-Custom-Header
 → JavaScript から読み取れるレスポンスヘッダーを追加する

プリフライトリクエスト(Preflight Request、OPTIONS

プリフライトリクエストとは

クロスオリジンリクエストを送る前に、ブラウザが自動的に事前確認(OPTIONSリクエスト) をサーバーに送る仕組み
サーバーが許可していることを確認してから本リクエストを送る

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

単純リクエスト(プリフライト不要)

以下の条件をすべて満たす場合:

メソッド:GET / HEAD / POST のみ
ヘッダー:以下のみ使用
 Accept / Accept-Language / Content-Language
 Content-Type(application/x-www-form-urlencoded /
               multipart/form-data / text/plain のみ)

プリフライトリクエスト

・PUT / DELETE / PATCH などのメソッドを使う
・Content-Type: application/json を使う
・Authorization などのカスタムヘッダーを付ける
・X-Custom-Header などの独自ヘッダーを付ける

プリフライトリクエストの流れ

① ブラウザが OPTIONS リクエストを自動送信
  OPTIONS /api/users HTTP/1.1
  Origin: https://app.example.com
  Access-Control-Request-Method: POST
  Access-Control-Request-Headers: Content-Type, Authorization

② サーバーが OPTIONS に対してレスポンスを返す
  HTTP/1.1 204 No Content
  Access-Control-Allow-Origin: https://app.example.com
  Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
  Access-Control-Allow-Headers: Content-Type, Authorization
  Access-Control-Max-Age: 86400

③ ブラウザが許可を確認して本リクエストを送信
  POST /api/users HTTP/1.1
  Origin: https://app.example.com
  Content-Type: application/json
  Authorization: Bearer eyJ...

④ サーバーが本リクエストを処理してレスポンス
  HTTP/1.1 201 Created
  Access-Control-Allow-Origin: https://app.example.com
  Content-Type: application/json

credentials(Cookie・認証情報)

credentials とは

Cookie・HTTP 認証・クライアント証明書などの認証情報をクロスオリジンリクエストに含めるかどうかを制御する設定

クライアント側の設定

// Cookie を一切送らない
fetch(url, { credentials: 'omit' })
// 同一オリジンのみ Cookie を送る
fetch(url, { credentials: 'same-origin' })
// クロスオリジンでも Cookie を送る
fetch(url, { credentials: 'include' })

サーバー側の設定

credentials: 'include' を使う場合:
- Access-Control-Allow-Credentials: true を付ける
- Access-Control-Allow-Origin に * は使えない(明示的なオリジンが必要)

Access-Control-Allow-Origin: https://app.example.com  ← 明示的に指定
Access-Control-Allow-Credentials: true

クロスオリジンリクエストで Cookie を送るには Cookie のSameSite属性も考慮が必要

SameSite=Strict → クロスオリジンでは Cookie を送らない
SameSite=Lax    → GET 系のナビゲーションのみ送る
SameSite=None   → クロスオリジンでも送る(Secure 属性必須)

クロスオリジン API に Cookie を送りたい場合:
Set-Cookie: session_id=abc123; SameSite=None; Secure; HttpOnly

fetch 設定

mode

3つのモードが存在する

fetch(url, { mode: 'cors' })
// → クロスオリジンリクエストを CORS で許可する(デフォルト)
// → サーバーが CORS ヘッダーを返さないとエラーになる

fetch(url, { mode: 'no-cors' })
// → CORS ヘッダーなしでリクエストを送る
// → レスポンスの中身は一切読めない(opaque response)

fetch(url, { mode: 'same-origin' })
// → クロスオリジンリクエストは必ずエラーにする
// → 同一オリジンのみ許可したい場合に使う

注意点

Access-Control-Allow-Origin: * と credentials の併用不可

# NG:明示的なオリジンでない
Access-Control-Allow-Origin: *

# OK:明示的なオリジン
Access-Control-Allow-Origin: https://app.example.com

環境ごとのオリジン管理

環境ごとのオリジンを環境変数で管理する

# NG:本番コードに localhost を直書きする
# → 本番環境にローカルホストへのアクセス許可が残ってしまう
origins 'https://app.example.com', 'http://localhost:3000'

# OK:環境変数で管理する
# config/initializers/cors.rb
allowed_origins = ENV.fetch('CORS_ALLOWED_ORIGINS', 'http://localhost:3000')
                     .split(',')
                     .map(&:strip)
                     
# .env.production
CORS_ALLOWED_ORIGINS=https://app.example.com
# .env.local
CORS_ALLOWED_ORIGINS=http://localhost:3000

OPTIONSリクエストに認証ミドルウェアをかけない

# NG:OPTIONS リクエストにも JWT 認証をかけてしまう
class ApplicationController < ActionController::API
  before_action :authenticate_user!  # OPTIONS にも実行されてしまう
end

# OK:OPTIONS は認証をスキップする
class ApplicationController < ActionController::API
  before_action :authenticate_user!

  private

  def authenticate_user!
    # プリフライトリクエストは認証をスキップする
    return if request.method == 'OPTIONS'
    # 以降の認証処理
  end
end

ロードバランサー・リバースプロキシでの CORS 設定の二重付与

アプリサーバー と Nginx(またはロードバランサー)の両方で CORS ヘッダーを付けると重複して付与されエラーになる
CORS ヘッダーはどちらか一方にだけ設定する

アプリサーバー(Rails, Laravel, Express など)のみで設定
 → Nginx では CORS を設定しない
           or
Nginx のみで設定
 → アプリサーバーでは CORS を設定しない

開発環境でのプロキシを使った回避

開発中で都度サーバーの設定を変えるのが面倒な場合、フロントエンドの開発サーバーにプロキシを設定することで CORS を回避可能

// /api/users などへのリクエストを localhost:8080/api に転送する
// 同一オリジンとして扱われるため CORS が発生しない

参考