#はじめに

「CORSエラーが出て、どうすればいいかわからない」——Web開発で最もよく遭遇するエラーの一つです。

前回学んだ同一オリジンポリシーは、セキュリティのために別オリジンへのアクセスを制限します。しかし、現代のWebアプリケーションでは、別オリジンのAPIを呼び出すことは日常的です。CORSは、この制限を安全に緩和するための仕組みです。

この記事を読むと、以下のことができるようになります:

  • CORSの仕組みを完全に理解できる
  • プリフライトリクエストの役割がわかる
  • CORSエラーの原因を特定し、対処できる

#CORSとは

CORS(Cross-Origin Resource Sharing)とは、サーバーが「このオリジンからのアクセスを許可する」と明示的に宣言することで、同一オリジンポリシーを緩和する仕組みです。

[ブラウザ: example.com]              [サーバー: api.other.com]
         |                                    |
         |------ GET /data ------------------>|
         |       Origin: https://example.com  |
         |                                    |
         |<----- Access-Control-Allow-Origin: |
         |       https://example.com          |
         |                                    |
         |  ブラウザ: 「許可されている、読み取りOK」

サーバーがAccess-Control-Allow-Originヘッダーで許可を示すと、ブラウザはレスポンスをJavaScriptに渡します。

#シンプルリクエストとプリフライト

CORSには2種類のリクエストパターンがあります。

#シンプルリクエスト

以下の条件をすべて満たすリクエストは「シンプルリクエスト」として扱われます:

条件許可される値
メソッドGET, HEAD, POST
ヘッダーAccept, Accept-Language, Content-Language, Content-Type のみ
Content-Typeapplication/x-www-form-urlencoded, multipart/form-data, text/plain

シンプルリクエストは、直接送信されます。

[ブラウザ]                           [サーバー]
    |                                    |
    |------ GET /data ------------------>|
    |       Origin: https://example.com  |
    |                                    |
    |<----- 200 OK ----------------------|
    |       Access-Control-Allow-Origin: |
    |       https://example.com          |

#プリフライトリクエスト

シンプルリクエストの条件を満たさないリクエストは、事前に「プリフライト」が送信されます。

[ブラウザ]                           [サーバー]
    |                                    |
    |====== プリフライト(OPTIONS)======|
    |                                    |
    |------ OPTIONS /data -------------->|
    |       Origin: https://example.com  |
    |       Access-Control-Request-Method: PUT
    |       Access-Control-Request-Headers: Content-Type
    |                                    |
    |<----- 204 No Content --------------|
    |       Access-Control-Allow-Origin: https://example.com
    |       Access-Control-Allow-Methods: PUT
    |       Access-Control-Allow-Headers: Content-Type
    |                                    |
    |====== 実際のリクエスト =============|
    |                                    |
    |------ PUT /data ------------------>|
    |       Origin: https://example.com  |
    |       Content-Type: application/json
    |                                    |
    |<----- 200 OK ----------------------|

#プリフライトが必要な例

// カスタムヘッダーを使用 → プリフライト発生
fetch('https://api.other.com/data', {
  headers: {
    'Authorization': 'Bearer token123',  // カスタムヘッダー
    'Content-Type': 'application/json'   // application/json
  }
});

// PUTメソッドを使用 → プリフライト発生
fetch('https://api.other.com/data', {
  method: 'PUT',
  body: JSON.stringify({ name: 'test' })
});

#CORSレスポンスヘッダー

サーバーがCORSを許可するために返すヘッダーです。

#Access-Control-Allow-Origin

許可するオリジンを指定します。

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

# すべてのオリジンを許可(認証情報がない場合のみ)
Access-Control-Allow-Origin: *

注意: *と認証情報(Cookie等)の送信は併用できません。

#Access-Control-Allow-Methods

許可するHTTPメソッドを指定します(プリフライト用)。

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

#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.other.com/data', {
  credentials: 'include'  // Cookieを送信
});

#Access-Control-Expose-Headers

JavaScript からアクセスできるレスポンスヘッダーを指定します。

デフォルトでアクセスできるのは以下のみ:

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

カスタムヘッダーを読み取るには、明示的に公開する必要があります:

Access-Control-Expose-Headers: X-Request-Id, X-Custom-Header

#Access-Control-Max-Age

プリフライトの結果をキャッシュする時間(秒)を指定します。

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

これにより、毎回プリフライトを送信する必要がなくなります。

#CORSリクエストヘッダー

ブラウザが自動的に付与するヘッダーです。

#Origin

リクエスト元のオリジンを示します。

Origin: https://example.com

#Access-Control-Request-Method

プリフライトで、実際のリクエストで使用するメソッドを示します。

Access-Control-Request-Method: PUT

#Access-Control-Request-Headers

プリフライトで、実際のリクエストで使用するヘッダーを示します。

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

#認証情報付きリクエスト

Cookieやセッション情報を含むリクエストには、追加の設定が必要です。

#クライアント側

fetch('https://api.other.com/data', {
  credentials: 'include'  // 必須
});

#サーバー側

Access-Control-Allow-Origin: https://example.com  # * は使えない
Access-Control-Allow-Credentials: true

重要: 認証情報を送信する場合、Access-Control-Allow-Origin: *は使用できません。具体的なオリジンを指定する必要があります。

#CORSエラーの対処法

#エラー1: No ‘Access-Control-Allow-Origin’ header

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

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

対処:

  1. サーバー側でAccess-Control-Allow-Originヘッダーを設定
  2. または、プロキシサーバー経由でアクセス

#エラー2: Origin is not allowed

Access to fetch at 'https://api.other.com/data' from origin
'https://example.com' has been blocked by CORS policy:
The 'Access-Control-Allow-Origin' header has a value 'https://allowed.com'
that is not equal to the supplied origin.

原因: サーバーが許可しているオリジンと、リクエスト元が異なる

対処: サーバーの許可リストにリクエスト元オリジンを追加

#エラー3: Credentials with wildcard

Access to fetch at 'https://api.other.com/data' from origin
'https://example.com' has been blocked by CORS policy:
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: *

対処: 具体的なオリジンを指定(*ではなくhttps://example.com

#エラー4: Method not allowed

Access to fetch at 'https://api.other.com/data' from origin
'https://example.com' has been blocked by CORS policy:
Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.

原因: プリフライトで返された許可メソッドに含まれていない

対処: Access-Control-Allow-Methodsに該当メソッドを追加

#エラー5: Header not allowed

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

原因: プリフライトで返された許可ヘッダーに含まれていない

対処: Access-Control-Allow-Headersに該当ヘッダーを追加

#サーバー実装例

#Express.js

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
}));

#nginx

location /api/ {
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' 'https://example.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
        add_header 'Access-Control-Max-Age' 86400;
        return 204;
    }

    add_header 'Access-Control-Allow-Origin' 'https://example.com';
    add_header 'Access-Control-Allow-Credentials' 'true';
}

#DevToolsでのデバッグ

#Networkタブで確認

  1. DevToolsのNetworkタブを開く
  2. プリフライト(OPTIONS)リクエストを探す
  3. リクエストとレスポンスのヘッダーを確認

確認ポイント:

  • リクエストにOriginヘッダーがあるか
  • レスポンスにAccess-Control-Allow-Originがあるか
  • 許可されたメソッド/ヘッダーが正しいか

#Consoleでエラーを確認

CORSエラーは詳細なメッセージが表示されます。エラーメッセージをよく読むと、何が問題かがわかります。

#まとめ

  • CORSは同一オリジンポリシーを安全に緩和する仕組み
  • シンプルリクエスト: 直接送信、レスポンスヘッダーで判断
  • プリフライトリクエスト: 事前にOPTIONSで許可を確認
  • 重要なヘッダー:
    • Access-Control-Allow-Origin: 許可するオリジン
    • Access-Control-Allow-Methods: 許可するメソッド
    • Access-Control-Allow-Headers: 許可するヘッダー
    • Access-Control-Allow-Credentials: 認証情報の許可
  • 認証情報付きリクエストでは * は使えない

#次のステップ

CORSを理解したところで、次はCSP(Content Security Policy)について学びましょう。CSPは、XSS攻撃などからWebアプリケーションを保護するための強力なセキュリティ機構です。

#参考リンク