CORS完全解説 - オリジン間リソース共有の仕組みとエラー対処法

CORS(Cross-Origin Resource Sharing)は、ブラウザが異なるオリジン間でのHTTPリクエストを制御する仕組み。サーバーが許可したオリジンのみがレスポンスを読み取れるようにする。

オリジンとは

オリジンは「スキーム + ホスト + ポート」の組み合わせ。1つでも異なれば**別オリジン(クロスオリジン)**となる。

https://example.com:443

スキーム: https
ホスト:   example.com
ポート:   443
URLhttps://example.com との関係
https://example.com/api同一オリジン(パスは無関係)
http://example.com別オリジン(スキームが違う)
https://api.example.com別オリジン(サブドメインが違う)
https://example.com:8080別オリジン(ポートが違う)
https://other.com別オリジン(ホストが違う)

同一オリジンポリシー

ブラウザは**同一オリジンポリシー(Same-Origin Policy)**により、スクリプトが別オリジンのリソースを読み取ることを制限している。これはXSSやCSRFなどの攻撃からユーザーを守るためのセキュリティ機構。

CORSはこの制約を、サーバー側が明示的に「このオリジンからのアクセスを許可する」と宣言することで緩和する仕組み。

単純リクエスト(Simple Request)

以下の条件を全て満たすリクエストは単純リクエストとして扱われ、プリフライトなしに送信される。

メソッド: GET / HEAD / POST のいずれか

リクエストヘッダー: 以下のみ許可

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Typeapplication/x-www-form-urlencoded / multipart/form-data / text/plain のいずれか)
sequenceDiagram
    participant B as ブラウザ
    participant S as サーバー
    B->>S: GET /api/data
    Note over B,S: Origin: https://example.com
    S-->>B: 200 OK
    Note over B,S: Access-Control-Allow-Origin: *

ブラウザは Origin ヘッダーを自動的に付与し、サーバーの Access-Control-Allow-Origin レスポンスヘッダーを確認する。オリジンが許可されていない場合、ブラウザはレスポンスをJavaScript から読めないようにブロックする(リクエスト自体は届いている)。

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

単純リクエストの条件を満たさない場合(例: Authorization ヘッダーを含む、Content-Type: application/json、PUT/PATCH/DELETE メソッドなど)、ブラウザはまず OPTIONS リクエストを送り、サーバーが許可しているか確認する。

sequenceDiagram
    participant B as ブラウザ
    participant S as サーバー
    rect rgb(230, 240, 255)
        Note over B,S: プリフライトリクエスト
        B->>S: OPTIONS /api/data
        Note over B,S: Origin / Access-Control-Request-Method: POST<br/>Access-Control-Request-Headers: Content-Type, Authorization
        S-->>B: 204 No Content
        Note over B,S: Access-Control-Allow-Origin: https://example.com<br/>Access-Control-Allow-Methods: GET, POST, PUT<br/>Access-Control-Allow-Headers: Content-Type, Authorization<br/>Access-Control-Max-Age: 86400
    end
    rect rgb(230, 255, 230)
        Note over B,S: 本リクエスト
        B->>S: POST /api/data
        Note over B,S: Origin / Content-Type: application/json<br/>Authorization: Bearer xxx
        S-->>B: 200 OK
    end

CORSレスポンスヘッダー

Access-Control-Allow-Origin

許可するオリジンを指定する。最も重要なヘッダー。

Access-Control-Allow-Origin: *                    # 全オリジンを許可
Access-Control-Allow-Origin: https://example.com  # 特定オリジンのみ許可

*(ワイルドカード)は Authorization ヘッダーやCookieを含むリクエスト(credentials: 'include')では使用できない。複数オリジンを許可する場合は、リクエストの Origin ヘッダーを見てサーバー側で動的に返す。

Access-Control-Allow-Methods

許可するHTTPメソッドを指定する。プリフライトレスポンスで使用。

Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS

Access-Control-Allow-Headers

許可するリクエストヘッダーを指定する。プリフライトレスポンスで使用。

Access-Control-Allow-Headers: Content-Type, Authorization, X-Custom-Header

Access-Control-Allow-Credentials

Cookieや Authorization ヘッダーを含むリクエストを許可する場合に必要。

Access-Control-Allow-Credentials: true

クライアント側でも credentials: 'include' の指定が必要。

fetch('https://api.example.com/data', {
  credentials: 'include',
})

credentials: 'include' を使う場合は Access-Control-Allow-Origin* は使えない。

Access-Control-Max-Age

プリフライトのキャッシュ時間(秒)。指定期間内は同じリクエストでプリフライトを省略できる。

Access-Control-Max-Age: 86400  # 24時間

Access-Control-Expose-Headers

デフォルトではブラウザが読める レスポンスヘッダーは限られている。追加のヘッダーを公開する場合に使用。

Access-Control-Expose-Headers: X-Total-Count, X-Request-Id

サーバー側の設定例

Node.js(Express)

const cors = require('cors')

// 全オリジン許可
app.use(cors())

// 特定オリジンのみ許可
app.use(cors({
  origin: 'https://example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
}))

// 複数オリジンを動的に許可
const allowedOrigins = ['https://example.com', 'https://app.example.com']
app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true)
    } else {
      callback(new Error('Not allowed by CORS'))
    }
  },
  credentials: true,
}))

Nginx

location /api/ {
    add_header Access-Control-Allow-Origin "https://example.com" always;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;

    if ($request_method = OPTIONS) {
        add_header Access-Control-Max-Age 86400;
        return 204;
    }
}

CloudFront(AWS)

CloudFrontでのCORS設定はオリジンリクエストポリシーで Origin ヘッダーを転送し、オリジンサーバーでCORSを設定するのが基本。

よくあるCORSエラーと対処法

エラー1: Access-Control-Allow-Origin がない

Access to fetch at 'https://api.example.com' from origin 'https://example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is
present on the requested resource.

原因: サーバーがCORSヘッダーを返していない

対処: サーバー側でCORSヘッダーを追加する


エラー2: Credentials を使う場合の *

The value of the 'Access-Control-Allow-Origin' header in the response must not
be the wildcard '*' when the request's credentials mode is 'include'.

原因: credentials: 'include' のときに Access-Control-Allow-Origin: * を返している

対処: オリジンを明示する

Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true

エラー3: プリフライトが失敗する

Response to preflight request doesn't pass access control check:
It does not have HTTP ok status.

原因: OPTIONS リクエストに対してサーバーが 200/204 を返していない

対処: OPTIONS メソッドを明示的に処理する

app.options('*', cors())  // Expressの場合

エラー4: 許可されていないヘッダー

Request header field Authorization is not allowed by
Access-Control-Allow-Headers in preflight response.

原因: プリフライトレスポンスの Access-Control-Allow-Headers に必要なヘッダーが含まれていない

対処: 必要なヘッダーを追加する

Access-Control-Allow-Headers: Content-Type, Authorization

よくある誤解

誤解1: CORSはサーバーへのリクエストをブロックする

実際はブラウザがレスポンスをJavaScriptから読めないようにするだけで、リクエスト自体はサーバーに届く。DELETEリクエストなどは実行されている場合がある。

誤解2: CORSを無効にすれば解決する

ブラウザの拡張機能などでCORSを無効にする方法は開発中のデバッグ用途のみ。本番環境では必ずサーバー側で適切に設定する。

誤解3: * を設定すれば全て解決する

credentials: 'include' との組み合わせは使えない。また、不必要な全オリジン許可はセキュリティリスクになる。

参考リンク