Skilore

CORS(Cross-Origin Resource Sharing)

CORSはブラウザのセキュリティ機構「同一オリジンポリシー」を安全に緩和する仕組み。プリフライトリクエスト、許可ヘッダー、Credentialsの設定を理解し、正しくCORSを構成する。

106 分で読めます52,551 文字

CORS(Cross-Origin Resource Sharing)

CORSはブラウザのセキュリティ機構「同一オリジンポリシー」を安全に緩和する仕組み。プリフライトリクエスト、許可ヘッダー、Credentialsの設定を理解し、正しくCORSを構成する。

前提知識

このガイドを読む前に、以下の知識があると理解が深まります:

  • HTTP基礎 — リクエスト/レスポンス、ヘッダー、ステータスコードの仕組み
  • ブラウザのセキュリティモデル — Same-Origin Policy、サンドボックス、セキュリティ境界
  • TLS/SSL — HTTPS通信の暗号化と証明書の基礎

CORSは同一オリジンポリシー(Same-Origin Policy)という根本的なWebセキュリティ機構を理解していないと本質を掴めない。ブラウザがなぜクロスオリジンリクエストを制限するのか、どのような攻撃を防いでいるのかを知ることで、CORSの設計意図と正しい設定方法が明確になる。


この章で学ぶこと

  • 同一オリジンポリシーの起源と目的を理解する
  • CORSの仕組みとブラウザの挙動を把握する
  • シンプルリクエストとプリフライトの違いを明確に区別する
  • サーバー側のCORS設定方法(Express, nginx, 各種フレームワーク)を学ぶ
  • Credentials(Cookie/認証情報)を伴うCORSの注意点を理解する
  • 開発環境と本番環境それぞれでのCORS戦略を習得する
  • CORSに関するセキュリティリスクとベストプラクティスを把握する

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

1.1 オリジンの定義

同一オリジンポリシーを理解するには、まず「オリジン」の定義を正確に把握する必要がある。オリジンは以下の3要素の組み合わせで決定される。

オリジン = スキーム + ホスト + ポート

  https://example.com:443/path/to/resource?query=value#fragment
  ↑        ↑            ↑    ↑                ↑         ↑
  スキーム   ホスト       ポート パス            クエリ     フラグメント
  (scheme)  (host)      (port) (path)         (query)   (fragment)

  ※ オリジンの判定に使われるのはスキーム・ホスト・ポートの3要素のみ
  ※ パス、クエリ、フラグメントはオリジンの判定に含まれない

1.2 同一オリジン判定の具体例

基準URL: https://www.example.com/page

比較対象                                結果      理由
───────────────────────────────────────────────────────────────
https://www.example.com/other          同一 ○    パスのみ異なる
https://www.example.com/page?q=1       同一 ○    クエリのみ異なる
https://www.example.com:443/page       同一 ○    HTTPSのデフォルトポート
http://www.example.com/page            異なる ✗  スキームが異なる
https://api.example.com/page           異なる ✗  ホスト(サブドメイン)が異なる
https://example.com/page               異なる ✗  ホスト(wwwの有無)が異なる
https://www.example.com:8443/page      異なる ✗  ポートが異なる
https://www.example.org/page           異なる ✗  ドメインが異なる

※ 重要: サブドメインが異なるだけでも別オリジンとなる
   www.example.com と api.example.com は別オリジン

1.3 同一オリジンポリシーの歴史的背景

同一オリジンポリシーは1995年にNetscape Navigator 2.02で初めて導入されたセキュリティモデルである。Webが発展するにつれ、異なるWebサイト間でのデータ窃取を防ぐ根本的なセキュリティ境界として機能してきた。

同一オリジンポリシーの保護モデル:

  攻撃シナリオ(SOPがない場合):
ユーザーが evil.com を閲覧中
evil.com のJS
── fetch("https://bank.com/api/balance") ──→
(ユーザーのCookieが自動送信される)
←── { balance: 1000000 } ───────────────────
── evil.com のサーバーに送信 ──→
(ユーザーの残高情報が盗まれる)
※ SOPがあるため、このレスポンスの読み取りは
ブラウザによってブロックされる

1.4 SOPの適用範囲

同一オリジンポリシーは全てのリソースに一律に適用されるわけではない。歴史的な理由から、以下のように適用範囲が異なる。

SOPの適用対象と非適用対象:
SOP が制限するもの
・fetch / XMLHttpRequest
・Canvas への他オリジン画像描画
(tainted canvas)
・Web Storage(localStorage等)
・IndexedDB
・Cookie(別途ルールあり)
・iframe の DOM アクセス
SOP が制限しないもの
・<img src="...">
・<script src="...">
・<link rel="stylesheet" ...>
・<video> / <audio>
・<form action="...">
・@font-face
・<iframe>(表示は可、DOM不可)
※ <script> や <img> がSOPの制限を受けないのは、
    Web初期からクロスオリジンでの利用が一般的だったため
  ※ ただし、これがJSONPやCSRF等の攻撃手法の温床にもなった

2. CORSの仕組み

2.1 CORSの全体像

CORS(Cross-Origin Resource Sharing)は、同一オリジンポリシーを安全に緩和するためのHTTPヘッダーベースの仕組みである。サーバーが「このオリジンからのアクセスを許可する」と明示的に宣言することで、ブラウザがクロスオリジンリクエストのレスポンスへのアクセスを許可する。

CORSの基本概念図:
ブラウザサーバー
https://apphttps://api
.example.com.example.com
フロントエンドバックエンド
(React等)(Express等)
①リクエスト── HTTP Request ──→
送信Origin: https://②オリジン
app.example.com検証
④レスポンス←── HTTP Response ──③CORSヘッダ
利用可否Access-Control-付与
判定Allow-Origin:
https://app...
重要: CORSはブラウザのセキュリティ機構
  → サーバー間通信(curl, サーバーサイドHTTPクライアント)には適用されない
  → ブラウザが「レスポンスをJSに渡すかどうか」を判断する仕組み
  → リクエスト自体はサーバーに到達する(※プリフライトを除く)

2.2 シンプルリクエスト(Simple Request)

シンプルリクエストは、プリフライトなしで直接サーバーに送信されるリクエストである。以下の全ての条件を満たす場合にシンプルリクエストとして扱われる。

シンプルリクエストの条件(全て満たす必要がある):
条件1: HTTPメソッド
GET, HEAD, POST のいずれか
条件2: ヘッダー(以下のみ許可)
・Accept
・Accept-Language
・Content-Language
・Content-Type(条件3を参照)
・Range(単純な範囲指定のみ)
条件3: Content-Type(以下のいずれか)
・application/x-www-form-urlencoded
・multipart/form-data
・text/plain
条件4: ReadableStream を使用していない
条件5: XMLHttpRequestUpload にイベントリスナーが
設定されていない
シンプルリクエストのフロー:

     ブラウザ                              サーバー
     │                                     │
     │── GET /api/public/data ──────→     │
     │   Host: api.example.com             │
     │   Origin: https://app.example.com   │
     │                                     │
     │                              ┌──────┤ オリジンを検証し
     │                              │      │ CORSヘッダーを付与
     │                              └──────┤
     │                                     │
     │←── 200 OK ──────────────────       │
     │   Access-Control-Allow-Origin:      │
     │     https://app.example.com         │
     │   Content-Type: application/json    │
     │                                     │
     │   {"data": "public info"}           │
     │                                     │

  ブラウザの判定:
  ・Allow-Origin がリクエストの Origin と一致 → JSにレスポンスを渡す
  ・Allow-Origin がない or 不一致 → CORSエラー(レスポンスを破棄)

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

シンプルリクエストの条件を満たさない場合、ブラウザは実際のリクエストを送信する前に、OPTIONSメソッドによるプリフライトリクエストを送信して、サーバーの許可を確認する。

プリフライトが発生する典型的なケース:

  ① HTTPメソッドが PUT / DELETE / PATCH
  ② Content-Type が application/json
  ③ カスタムヘッダーを使用(Authorization, X-Custom-Header 等)
  ④ 上記の組み合わせ

プリフライトの詳細シーケンス:

     ブラウザ                              サーバー
     │                                     │
     │  ※ fetch() でPUTリクエストを         │
     │    発行しようとする                   │
     │                                     │
     │  [Phase 1: プリフライト]             │
     │                                     │
     │── OPTIONS /api/users/123 ────→     │
     │   Host: api.example.com             │
     │   Origin: https://app.example.com   │
     │   Access-Control-Request-Method:    │
     │     PUT                             │
     │   Access-Control-Request-Headers:   │
     │     Content-Type, Authorization     │
     │                                     │
     │                              ┌──────┤
     │                              │ 許可  │
     │                              │ 判定  │
     │                              └──────┤
     │                                     │
     │←── 204 No Content ──────────       │
     │   Access-Control-Allow-Origin:      │
     │     https://app.example.com         │
     │   Access-Control-Allow-Methods:     │
     │     GET, POST, PUT, DELETE          │
     │   Access-Control-Allow-Headers:     │
     │     Content-Type, Authorization     │
     │   Access-Control-Max-Age: 86400     │
     │                                     │
     │  ※ プリフライト成功                  │
     │  ※ Max-Ageの間はキャッシュされる     │
     │                                     │
     │  [Phase 2: 実際のリクエスト]         │
     │                                     │
     │── PUT /api/users/123 ────────→     │
     │   Host: api.example.com             │
     │   Origin: https://app.example.com   │
     │   Content-Type: application/json    │
     │   Authorization: Bearer eyJhbG...   │
     │                                     │
     │   {"name": "Updated Name"}          │
     │                                     │
     │←── 200 OK ──────────────────       │
     │   Access-Control-Allow-Origin:      │
     │     https://app.example.com         │
     │   Content-Type: application/json    │
     │                                     │
     │   {"id": 123, "name": "Updated"}    │
     │                                     │

  注意: プリフライトが失敗した場合、実際のリクエストは送信されない
  → サーバー側でOPTIONSリクエストのハンドリングが必須

2.4 プリフライトキャッシュ

プリフライトリクエストは毎回のリクエストごとに送信されると、パフォーマンスに影響を与える。Access-Control-Max-Age ヘッダーにより、プリフライト結果をキャッシュできる。

プリフライトキャッシュの仕組み:

  Max-Age: 86400(24時間)の場合

  時刻 00:00  最初のリクエスト
  ├── OPTIONS /api/data ──→ (プリフライト送信)
  ├── 204 応答 ←──
  ├── PUT /api/data ──→ (実際のリクエスト)
  └── 200 応答 ←──

  時刻 01:00  2回目のリクエスト
  ├── (プリフライトはキャッシュヒット → 送信不要)
  ├── PUT /api/data ──→ (直接送信)
  └── 200 応答 ←──

  時刻 12:00  3回目のリクエスト
  ├── (まだキャッシュ有効)
  ├── DELETE /api/data ──→
  └── 200 応答 ←──

  時刻 24:01  キャッシュ期限切れ後
  ├── OPTIONS /api/data ──→ (再度プリフライト送信)
  ├── 204 応答 ←──
  ├── PUT /api/data ──→
  └── 200 応答 ←──

  ブラウザごとの Max-Age 上限:
ブラウザ上限値
Chrome/Edge7200秒(2時間)
Firefox86400秒(24h)
Safari604800秒(7日)
※ サーバーが Max-Age: 86400 を返しても、Chromeでは
    7200秒に切り詰められる
  ※ Max-Age を省略した場合、デフォルトは5秒

3. CORSヘッダー詳細

3.1 レスポンスヘッダー一覧

CORSレスポンスヘッダー完全リファレンス:
ヘッダー説明・用途
Access-Control-Allow-Origin許可するオリジンを指定
値: 特定オリジン or *
例: https://app.example.com
※ 複数オリジンは直接指定不可
Access-Control-Allow-Methods許可するHTTPメソッドを列挙
プリフライトレスポンスで使用
例: GET, POST, PUT, DELETE
Access-Control-Allow-Headers許可するリクエストヘッダーを列挙
プリフライトレスポンスで使用
例: Content-Type, Authorization
Access-Control-Expose-HeadersJSからアクセス可能なレスポンス
ヘッダーを指定
デフォルトで公開: Content-Type,
Cache-Control, Expires 等
例: X-Request-Id, X-Total-Count
Access-Control-Allow-Credentials(Cookie、認証情報)
Credentialsの送信を許可するか
値: true のみ(falseは省略)
※ Allow-Origin: * との併用不可
Access-Control-Max-Ageプリフライト結果のキャッシュ秒数
値: 秒数(整数)
例: 86400(24時間)
※ ブラウザごとに上限あり

3.2 リクエストヘッダー一覧

CORSリクエストヘッダー(ブラウザが自動付与):
ヘッダー説明・用途
Originリクエスト元のオリジン
ブラウザが自動的に付与
JSから変更不可
例: https://app.example.com
Access-Control-Request-Method実際に使用するHTTPメソッド
プリフライト(OPTIONS)で使用
例: PUT
Access-Control-Request-Headers実際に使用するヘッダー一覧
プリフライト(OPTIONS)で使用
例: Content-Type, Authorization
※ これらのヘッダーはブラウザが自動で設定する
  ※ JavaScript(fetch API等)からこれらを手動設定することはできない
  ※ Origin ヘッダーの偽装は通常のブラウザでは不可能

3.3 Credentials(資格情報)とCORS

Cookie、Authorization ヘッダー、TLSクライアント証明書などの資格情報を伴うクロスオリジンリクエストには特別なルールが適用される。

Credentialsモードの設定:

  fetch APIの場合:
credentials 値動作
"omit"Cookieを一切送信しない
レスポンスのCookieも無視
"same-origin"(既定)同一オリジンのみCookieを送信
クロスオリジンでは送信しない
"include"クロスオリジンでもCookieを送信
サーバー側の許可が必須
Credentials使用時の制約:
credentials: "include" を使用する場合
サーバーは以下を全て満たす必要がある:
1. Access-Control-Allow-Credentials: true
2. Access-Control-Allow-Origin: (特定のオリジン)
※ ワイルドカード "*" は使用不可
3. Access-Control-Allow-Headers: (特定のヘッダー)
※ ワイルドカード "*" は使用不可
4. Access-Control-Allow-Methods: (特定のメソッド)
※ ワイルドカード "*" は使用不可
5. Access-Control-Expose-Headers: (特定のヘッダー)
※ ワイルドカード "*" は使用不可
※ Credentials モードでは全てのワイルドカード指定が無効になる
  ※ これはセキュリティ上の重要な制約

4. サーバー側の設定

4.1 Express.js での CORS 設定

// ============================================================
// Express.js CORS設定 - 完全版
// ============================================================
 
import express from 'express';
import cors from 'cors';
 
const app = express();
 
// --------------------------------------------------
// 方法1: cors ミドルウェア(推奨)
// --------------------------------------------------
 
// 基本設定
app.use(cors({
  // 許可するオリジンのリスト
  origin: [
    'https://app.example.com',
    'https://admin.example.com',
    'https://staging.example.com',
  ],
  // 許可するHTTPメソッド
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  // 許可するリクエストヘッダー
  allowedHeaders: [
    'Content-Type',
    'Authorization',
    'X-Requested-With',
    'X-Request-Id',
  ],
  // JSからアクセス可能にするレスポンスヘッダー
  exposedHeaders: [
    'X-Total-Count',
    'X-Request-Id',
    'X-RateLimit-Remaining',
  ],
  // Credentials(Cookie等)を許可
  credentials: true,
  // プリフライト結果のキャッシュ時間(秒)
  maxAge: 86400,
  // OPTIONSリクエストに対して204を返す(デフォルト: 204)
  optionsSuccessStatus: 204,
}));
 
// --------------------------------------------------
// 方法2: 動的オリジン検証(パターンマッチング)
// --------------------------------------------------
 
const corsOptionsWithDynamicOrigin = cors({
  origin: (origin, callback) => {
    // origin が undefined の場合は同一オリジンリクエスト
    // (またはサーバー間通信)
    if (!origin) {
      return callback(null, true);
    }
 
    // ホワイトリスト方式
    const whitelist = [
      'https://app.example.com',
      'https://admin.example.com',
    ];
 
    if (whitelist.includes(origin)) {
      return callback(null, true);
    }
 
    // サブドメインのパターンマッチ
    const subdomainPattern = /^https:\/\/[\w-]+\.example\.com$/;
    if (subdomainPattern.test(origin)) {
      return callback(null, true);
    }
 
    // 開発環境のlocalhostを許可
    if (process.env.NODE_ENV === 'development') {
      const localhostPattern = /^http:\/\/localhost:\d+$/;
      if (localhostPattern.test(origin)) {
        return callback(null, true);
      }
    }
 
    // 許可されないオリジン
    callback(new Error(`Origin ${origin} is not allowed by CORS`));
  },
  credentials: true,
  maxAge: 3600,
});
 
app.use(corsOptionsWithDynamicOrigin);
 
// --------------------------------------------------
// 方法3: ルート単位でのCORS設定
// --------------------------------------------------
 
// 公開APIはワイルドカード許可
app.get('/api/public/*', cors({ origin: '*' }), (req, res) => {
  res.json({ data: 'public data' });
});
 
// プライベートAPIは特定オリジンのみ
const privateCors = cors({
  origin: 'https://app.example.com',
  credentials: true,
});
 
app.get('/api/private/*', privateCors, (req, res) => {
  res.json({ data: 'private data' });
});
 
// --------------------------------------------------
// 方法4: 手動実装(ミドルウェアを使わない場合)
// --------------------------------------------------
 
app.use((req, res, next) => {
  const allowedOrigins = [
    'https://app.example.com',
    'https://admin.example.com',
  ];
  const origin = req.headers.origin;
 
  // オリジンの検証
  if (origin && allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    // Vary ヘッダーを設定(CDNキャッシュ対策)
    res.setHeader('Vary', 'Origin');
  }
 
  res.setHeader(
    'Access-Control-Allow-Methods',
    'GET, POST, PUT, PATCH, DELETE, OPTIONS'
  );
  res.setHeader(
    'Access-Control-Allow-Headers',
    'Content-Type, Authorization, X-Requested-With'
  );
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  res.setHeader('Access-Control-Max-Age', '86400');
  res.setHeader(
    'Access-Control-Expose-Headers',
    'X-Total-Count, X-Request-Id'
  );
 
  // プリフライトリクエストへの応答
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }
 
  next();
});

4.2 nginx での CORS 設定

# ============================================================
# nginx CORS設定 - 本番環境向け完全版
# ============================================================
 
# オリジンのホワイトリストを map で定義
map $http_origin $cors_origin {
    default "";
    "https://app.example.com"     "https://app.example.com";
    "https://admin.example.com"   "https://admin.example.com";
    "https://staging.example.com" "https://staging.example.com";
    # 正規表現も使用可能
    ~^https://[\w-]+\.example\.com$  $http_origin;
}
 
server {
    listen 443 ssl;
    server_name api.example.com;
 
    location /api/ {
        # プリフライトリクエスト(OPTIONS)の処理
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin'
                       $cors_origin always;
            add_header 'Access-Control-Allow-Methods'
                       'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers'
                       'Content-Type, Authorization, X-Requested-With' always;
            add_header 'Access-Control-Allow-Credentials'
                       'true' always;
            add_header 'Access-Control-Max-Age'
                       86400 always;
            add_header 'Content-Type'
                       'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }
 
        # 通常リクエストへのCORSヘッダー付与
        add_header 'Access-Control-Allow-Origin'
                   $cors_origin always;
        add_header 'Access-Control-Allow-Credentials'
                   'true' always;
        add_header 'Access-Control-Expose-Headers'
                   'X-Total-Count, X-Request-Id' always;
        add_header 'Vary' 'Origin' always;
 
        proxy_pass http://backend_upstream;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
 
    # 公開アセット(CORSヘッダーなし or ワイルドカード)
    location /static/ {
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Cache-Control' 'public, max-age=31536000';
        root /var/www/assets;
    }
}

4.3 fetch API によるクロスオリジンリクエスト

// ============================================================
// fetch API CORS リクエスト例
// ============================================================
 
// --- 例1: シンプルリクエスト(GETでJSONを取得) ---
async function fetchPublicData(): Promise<void> {
  try {
    const response = await fetch('https://api.example.com/api/public/data', {
      method: 'GET',
      // シンプルリクエストの条件を満たすため
      // プリフライトは発生しない
    });
 
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
 
    const data = await response.json();
    console.log('取得成功:', data);
  } catch (error) {
    if (error instanceof TypeError && error.message === 'Failed to fetch') {
      // CORSエラーの場合、TypeError が発生する
      console.error('CORSエラーまたはネットワークエラー');
    } else {
      console.error('その他のエラー:', error);
    }
  }
}
 
// --- 例2: Credentials付きリクエスト(Cookie送信) ---
async function fetchWithCredentials(): Promise<void> {
  const response = await fetch('https://api.example.com/api/user/profile', {
    method: 'GET',
    credentials: 'include', // Cookie を送信
    // credentials: 'include' を指定した場合、
    // サーバーは Access-Control-Allow-Credentials: true を返す必要がある
    // かつ Allow-Origin に * は使用不可
  });
 
  const profile = await response.json();
  console.log('プロフィール:', profile);
}
 
// --- 例3: プリフライトが発生するリクエスト ---
async function updateUser(userId: number, data: object): Promise<void> {
  const response = await fetch(
    `https://api.example.com/api/users/${userId}`,
    {
      method: 'PUT',                      // → プリフライト発生(シンプルでない)
      headers: {
        'Content-Type': 'application/json', // → プリフライト発生
        'Authorization': 'Bearer eyJhbG...', // → プリフライト発生
        'X-Request-Id': crypto.randomUUID(), // → プリフライト発生
      },
      credentials: 'include',
      body: JSON.stringify(data),
    }
  );
 
  // レスポンスヘッダーへのアクセス
  // ※ Expose-Headers に含まれるヘッダーのみ取得可能
  const requestId = response.headers.get('X-Request-Id');
  const totalCount = response.headers.get('X-Total-Count');
 
  const result = await response.json();
  console.log('更新結果:', result);
}
 
// --- 例4: AbortController によるタイムアウト付きCORSリクエスト ---
async function fetchWithTimeout(
  url: string,
  timeoutMs: number = 5000
): Promise<Response> {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
 
  try {
    const response = await fetch(url, {
      method: 'GET',
      credentials: 'include',
      signal: controller.signal,
      headers: {
        'Accept': 'application/json',
      },
    });
    return response;
  } finally {
    clearTimeout(timeoutId);
  }
}

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

5.1 エラーパターン一覧

CORSエラーはブラウザのコンソールに表示されるが、セキュリティ上の理由から詳細なエラー情報はJavaScriptからは取得できない。以下に代表的なエラーパターンと対処法を網羅する。

エラーパターン比較表:
#コンソールメッセージ(要約)原因対処法
1No 'Access-Control-Allow-サーバーが CORSサーバーに Allow-Origin
Origin' header is presentヘッダーを返してヘッダーを設定する
いない
2The value of the 'Access-Allow-Origin の値がOrigin と完全一致する値を
Control-Allow-Origin' headerリクエストの Origin返す、またはホワイトリスト
must not be the wildcard '*'と一致しない、で動的に設定
when credentials mode iscredentials使用時に
'include'* を使用している
3Response to preflightOPTIONS リクエストOPTIONS メソッドの
request doesn't pass accessに対する応答が不正ハンドラーを実装する
control check
4Method PUT is not allowed byAllow-Methods に使用するメソッドを
Access-Control-Allow-Methods必要なメソッドがAllow-Methods に追加
含まれていない
5Request header fieldAllow-Headers に使用するヘッダーを
Authorization is not allowed必要なヘッダーがAllow-Headers に追加
by Access-Control-Allow-含まれていない
Headers
6Redirect is not allowed forプリフライトの応答OPTIONS に対して直接
a preflight requestがリダイレクト応答する(リダイレクト不可)
(301/302) を返した

5.2 デバッグ手順

CORSエラーが発生した場合の体系的なデバッグ手順を示す。

CORSエラーのデバッグフローチャート:

  START: CORSエラーがコンソールに表示された
    │
    ├── Step 1: ブラウザのDevToolsでNetworkタブを確認
    │   │
    │   ├── OPTIONSリクエストがあるか?
    │   │   ├── YES → プリフライトの応答を確認(Step 2a)
    │   │   └── NO  → シンプルリクエストの応答を確認(Step 2b)
    │   │
    │   Step 2a: プリフライト応答の確認
    │   ├── ステータスコードは 200 or 204 か?
    │   │   ├── NO → OPTIONSハンドラーを実装/修正
    │   │   └── YES → レスポンスヘッダーを確認
    │   │       ├── Allow-Origin は正しいか?
    │   │       ├── Allow-Methods に必要なメソッドがあるか?
    │   │       └── Allow-Headers に必要なヘッダーがあるか?
    │   │
    │   Step 2b: レスポンスヘッダーの確認
    │   ├── Allow-Origin ヘッダーが存在するか?
    │   │   ├── NO → サーバーにCORS設定を追加
    │   │   └── YES → 値がリクエストのOriginと一致するか確認
    │   │
    │   Step 3: Credentials関連の確認
    │   ├── credentials: 'include' を使用しているか?
    │   │   ├── YES → Allow-Origin が * になっていないか確認
    │   │   │         Allow-Credentials: true があるか確認
    │   │   └── NO → Step 4 へ
    │   │
    │   Step 4: curl で直接サーバーの応答を確認
    │       $ curl -v -X OPTIONS \
    │         -H "Origin: https://app.example.com" \
    │         -H "Access-Control-Request-Method: PUT" \
    │         https://api.example.com/api/data
    │
    └── END: 原因特定 → 修正 → ブラウザキャッシュクリア → 再検証

5.3 curl によるCORSデバッグコマンド

# ============================================================
# curl を使った CORS デバッグ
# ============================================================
 
# --- プリフライトリクエストのシミュレーション ---
curl -v -X OPTIONS \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: PUT" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization" \
  https://api.example.com/api/users
 
# 期待される応答ヘッダー:
# < HTTP/2 204
# < access-control-allow-origin: https://app.example.com
# < access-control-allow-methods: GET, POST, PUT, DELETE
# < access-control-allow-headers: Content-Type, Authorization
# < access-control-max-age: 86400
 
# --- シンプルリクエストのシミュレーション ---
curl -v \
  -H "Origin: https://app.example.com" \
  https://api.example.com/api/public/data
 
# --- Credentials付きリクエストのシミュレーション ---
curl -v \
  -H "Origin: https://app.example.com" \
  -H "Cookie: session=abc123" \
  https://api.example.com/api/user/profile
 
# 期待される応答ヘッダー:
# < access-control-allow-origin: https://app.example.com
# < access-control-allow-credentials: true
# (Allow-Origin が * だとブラウザではエラーになる)

6. 開発環境でのCORS対策

6.1 プロキシによる回避(推奨)

開発環境では、CORS自体を回避するアプローチが最もトラブルが少ない。フロントエンドの開発サーバーにプロキシを設定し、APIリクエストを中継させることで同一オリジンとなり、CORSが不要になる。

プロキシの仕組み:

  従来(CORS必要):
ブラウザ──── 直接通信 ───→APIサーバー
localhost:5173異なるオリジンlocalhost:8080
←── CORSエラー ──
プロキシ使用(CORS不要):
ブラウザ──→Vite Dev──→APIサーバー
localhost:5173Serverlocalhost:8080
/api → proxy
/api/users/api/users
は同一オリジン←──←──
ブラウザから見ると /api/users は localhost:5173 への
  リクエストなので、同一オリジン → CORSは発生しない
// ============================================================
// Vite のプロキシ設定(vite.config.ts)
// ============================================================
 
import { defineConfig } from 'vite';
 
export default defineConfig({
  server: {
    port: 5173,
    proxy: {
      // /api で始まるリクエストをバックエンドに転送
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        // リクエストパスの書き換え(必要な場合)
        // rewrite: (path) => path.replace(/^\/api/, ''),
      },
 
      // WebSocket のプロキシ
      '/ws': {
        target: 'ws://localhost:8080',
        ws: true,
      },
 
      // 複数のバックエンドへの振り分け
      '/auth': {
        target: 'http://localhost:9000',
        changeOrigin: true,
      },
    },
  },
});
// ============================================================
// webpack-dev-server のプロキシ設定(webpack.config.js)
// ============================================================
 
module.exports = {
  devServer: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        pathRewrite: { '^/api': '/api' },
        // エラーハンドリング
        onError: (err, req, res) => {
          console.error('Proxy error:', err);
          res.writeHead(502, { 'Content-Type': 'text/plain' });
          res.end('Bad Gateway: Backend server is not responding');
        },
      },
    },
  },
};
// ============================================================
// Next.js のリライト設定(next.config.js)
// ============================================================
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'http://localhost:8080/api/:path*',
      },
    ];
  },
};
 
module.exports = nextConfig;

6.2 環境別CORS設定パターン

// ============================================================
// 環境別CORS設定(Express.js)
// ============================================================
 
import cors from 'cors';
 
function getCorsOptions(): cors.CorsOptions {
  const env = process.env.NODE_ENV || 'development';
 
  switch (env) {
    case 'production':
      return {
        origin: [
          'https://app.example.com',
          'https://admin.example.com',
        ],
        credentials: true,
        maxAge: 86400,
      };
 
    case 'staging':
      return {
        origin: [
          'https://staging-app.example.com',
          'https://staging-admin.example.com',
        ],
        credentials: true,
        maxAge: 3600,
      };
 
    case 'development':
      return {
        origin: (origin, callback) => {
          // 開発環境では localhost の任意のポートを許可
          if (!origin || /^http:\/\/localhost:\d+$/.test(origin)) {
            callback(null, true);
          } else {
            callback(new Error('Not allowed by CORS'));
          }
        },
        credentials: true,
        maxAge: 0, // キャッシュなし(デバッグ容易性のため)
      };
 
    case 'test':
      return {
        origin: '*', // テスト環境では全オリジン許可
        credentials: false,
      };
 
    default:
      return {
        origin: false, // CORS無効(安全側に倒す)
      };
  }
}
 
app.use(cors(getCorsOptions()));

7. フレームワーク別CORS設定

7.1 主要フレームワーク比較表

フレームワーク別CORS設定方法の比較:
フレームワーク設定方法特徴
Express.jscors ミドルウェア最も柔軟、動的オリジン対応
or 手動ミドルウェアnpm: cors パッケージ
Fastify@fastify/corsExpress cors と類似のAPI
プラグインスキーマバリデーション対応
Honocors() ミドルウェア軽量、Edge Runtime対応
(hono/cors)Cloudflare Workers等で利用
Djangodjango-cors-headerssettings.py で一括設定
パッケージCORS_ALLOWED_ORIGINS等
Flaskflask-cors 拡張デコレータ or アプリ全体設定
リソース単位での設定可能
Spring Boot@CrossOriginアノテーションベース
WebMvcConfigurerグローバル or コントローラ単位
ASP.NET Coreservices.AddCors()ポリシーベース
app.UseCors()名前付きポリシー対応
Go (net/http)手動実装 orrs/cors パッケージが一般的
rs/cors パッケージミドルウェアパターン
Rust (Actix)actix-cors クレートビルダーパターンで設定
tower-http でも利用可能

7.2 Hono(Edge Runtime向け)

// ============================================================
// Hono CORS設定(Cloudflare Workers / Deno Deploy 等)
// ============================================================
 
import { Hono } from 'hono';
import { cors } from 'hono/cors';
 
const app = new Hono();
 
// グローバルCORS設定
app.use('/api/*', cors({
  origin: [
    'https://app.example.com',
    'https://admin.example.com',
  ],
  allowHeaders: [
    'Content-Type',
    'Authorization',
    'X-Request-Id',
  ],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  exposeHeaders: ['X-Total-Count', 'X-Request-Id'],
  credentials: true,
  maxAge: 86400,
}));
 
// 動的オリジン検証
app.use('/api/v2/*', cors({
  origin: (origin) => {
    // サブドメインパターンマッチ
    if (/^https:\/\/[\w-]+\.example\.com$/.test(origin)) {
      return origin;
    }
    return null; // 許可しない
  },
  credentials: true,
}));
 
// 公開APIはワイルドカード
app.use('/api/public/*', cors({
  origin: '*',
  maxAge: 3600,
}));
 
app.get('/api/data', (c) => {
  return c.json({ message: 'Hello from Edge!' });
});
 
export default app;

7.3 Django での CORS 設定

# ============================================================
# Django CORS設定(django-cors-headers)
# ============================================================
 
# settings.py
 
INSTALLED_APPS = [
    # ...
    'corsheaders',
    # ...
]
 
MIDDLEWARE = [
    # CORSミドルウェアは可能な限り上位に配置
    # (他のミドルウェアがレスポンスを変更する前にCORSヘッダーを付与するため)
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    # ...
]
 
# --- 許可するオリジン ---
CORS_ALLOWED_ORIGINS = [
    'https://app.example.com',
    'https://admin.example.com',
]
 
# 正規表現による許可(サブドメイン対応)
CORS_ALLOWED_ORIGIN_REGEXES = [
    r'^https://[\w-]+\.example\.com$',
]
 
# --- 許可するメソッド ---
CORS_ALLOW_METHODS = [
    'DELETE',
    'GET',
    'OPTIONS',
    'PATCH',
    'POST',
    'PUT',
]
 
# --- 許可するヘッダー ---
CORS_ALLOW_HEADERS = [
    'accept',
    'authorization',
    'content-type',
    'x-csrftoken',
    'x-requested-with',
]
 
# --- Credentials ---
CORS_ALLOW_CREDENTIALS = True
 
# --- プリフライトキャッシュ ---
CORS_PREFLIGHT_MAX_AGE = 86400
 
# --- Expose Headers ---
CORS_EXPOSE_HEADERS = [
    'x-total-count',
    'x-request-id',
]

7.4 Go(net/http + rs/cors)

// ============================================================
// Go CORS設定(github.com/rs/cors)
// ============================================================
 
package main
 
import (
    "log"
    "net/http"
 
    "github.com/rs/cors"
)
 
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/data", handleData)
    mux.HandleFunc("/api/users", handleUsers)
 
    // CORS設定
    c := cors.New(cors.Options{
        AllowedOrigins: []string{
            "https://app.example.com",
            "https://admin.example.com",
        },
        AllowedMethods: []string{
            "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS",
        },
        AllowedHeaders: []string{
            "Content-Type",
            "Authorization",
            "X-Request-Id",
        },
        ExposedHeaders: []string{
            "X-Total-Count",
            "X-Request-Id",
        },
        AllowCredentials: true,
        MaxAge:           86400,
        // デバッグモード(開発時のみ有効にする)
        Debug: false,
    })
 
    handler := c.Handler(mux)
 
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", handler))
}
 
func handleData(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte(`{"message": "Hello from Go!"}`))
}
 
func handleUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("X-Total-Count", "42")
    w.Write([]byte(`{"users": []}`))
}

8. セキュリティ上の考慮事項

8.1 アンチパターン

CORS設定における代表的なアンチパターンを理解し、セキュリティリスクを回避する。

アンチパターン1: Origin をそのまま反射(Origin Reflection)

  ✗ 危険な実装:

  app.use((req, res, next) => {
    // リクエストの Origin をそのまま Allow-Origin に返す
    res.setHeader(
      'Access-Control-Allow-Origin',
      req.headers.origin  // ← 検証なし!
    );
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    next();
  });

  問題点:
1. 任意のオリジンからのリクエストが許可される
2. Credentials: true との組み合わせで致命的
→ 攻撃者のサイトからユーザーのCookieを使って
APIにアクセスし、レスポンスを読み取れる
3. Allow-Origin: * と同等だが、Credentials も許可
されるため、実質的に * より危険
✓ 正しい実装:

  const ALLOWED_ORIGINS = new Set([
    'https://app.example.com',
    'https://admin.example.com',
  ]);

  app.use((req, res, next) => {
    const origin = req.headers.origin;
    if (origin && ALLOWED_ORIGINS.has(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin);
      res.setHeader('Access-Control-Allow-Credentials', 'true');
    }
    next();
  });
アンチパターン2: 本番環境での Access-Control-Allow-Origin: *

  ✗ 危険な使用例:

  // 本番環境の認証付きAPIで * を使用
  app.use(cors({
    origin: '*',
    // credentials: true は * と併用できないため
    // Cookie送信は不可だが、以下の問題がある
  }));

  問題点:
1. 意図しないオリジンからデータを取得される可能性
2. Bearer トークン認証の場合、トークンをヘッダーで
送信すれば任意のオリジンからアクセス可能
3. 内部APIが外部から叩かれるリスク
4. レート制限やIP制限をバイパスされる可能性
✓ * が許容されるケース:
・完全に公開のAPI(認証なし、機密データなし)
・CDNで配信する静的アセット(画像、フォント等)
・公開データセットのAPI(政府オープンデータ等)
・OEmbed等の埋め込み用エンドポイント

8.2 Vary ヘッダーの重要性

CDNキャッシュとCORSの落とし穴:

  シナリオ: CDNがCORSレスポンスをキャッシュする場合

  ① https://app-a.example.com がリクエスト
     → サーバーが Allow-Origin: https://app-a.example.com を返す
     → CDNがこのレスポンスをキャッシュ

  ② https://app-b.example.com が同じURLにリクエスト
     → CDNがキャッシュを返す
     → Allow-Origin: https://app-a.example.com(app-aのまま!)
     → app-b ではCORSエラーになる

  対策: Vary: Origin ヘッダーを設定

  res.setHeader('Vary', 'Origin');
  // → CDNはOriginヘッダーの値ごとに異なるキャッシュを保持
  // → app-a用とapp-b用で別々のキャッシュが作られる

  Varyヘッダーのフロー:
CDN キャッシュ
Key: URL + Origin
/api/data
/api/data
/api/data

8.3 null オリジンの危険性

null オリジンの注意点:

  Origin: null が送信されるケース:
・file:// プロトコルからのリクエスト
・data: URI からのリクエスト
・サンドボックス化された iframe
・リダイレクトチェーン中のリクエスト
・ブラウザのプライバシー保護機能による匿名化
✗ 危険な実装:

  // null オリジンを許可してしまう
  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }
  // allowedOrigins に 'null'(文字列)が含まれている場合、
  // data: URI や sandboxed iframe からのアクセスが可能になる

  ✓ 安全な実装:

  // null オリジンは明示的に拒否
  if (origin && origin !== 'null' && allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }

FAQ(よくある質問)

Q1: CORSエラーの一般的な解決方法 — エラーメッセージから原因を特定する

■ 代表的なCORSエラーと解決策:

エラー1: "has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header"

  原因: サーバーがAccess-Control-Allow-Originヘッダーを返していない

  解決策:
  // Express.js
  app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
    next();
  });

  // nginx
  add_header Access-Control-Allow-Origin https://app.example.com;

  // 開発環境のみ(本番環境では禁止)
  res.setHeader('Access-Control-Allow-Origin', '*');

エラー2: "The value of the 'Access-Control-Allow-Origin' header must not be '*' when credentials mode is 'include'"

  原因: credentials: 'include'(Cookie送信)を使用している場合、
        ワイルドカード(*)は使用不可

  解決策:
  // 特定のオリジンを明示
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
  }

エラー3: "has been blocked by CORS policy: Method POST is not allowed by Access-Control-Allow-Methods"

  原因: プリフライトリクエストで許可メソッドが返されていない

  解決策:
  app.options('/api/*', (req, res) => {
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
    res.status(204).send();
  });

エラー4: "Request header field authorization is not allowed by Access-Control-Allow-Headers"

  原因: カスタムヘッダー(Authorization等)が許可されていない

  解決策:
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Request-Id');

エラー5: "CORS policy: Response to preflight request doesn't pass access control check: status is 401"

  原因: プリフライトリクエスト(OPTIONS)に認証を要求している

  解決策:
  // OPTIONSリクエストは認証不要にする
  app.options('/api/*', (req, res) => {
    // 認証チェックをスキップ
    res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.status(204).send();
  });

デバッグ手順:
  1. ブラウザのDevToolsでNetworkタブを開く
  2. プリフライト(OPTIONS)が成功しているか確認
  3. レスポンスヘッダーを確認:
     ・Access-Control-Allow-Origin が正しいか
     ・Access-Control-Allow-Methods が含まれているか
     ・Access-Control-Allow-Headers が含まれているか
  4. curlで直接確認:
     curl -I -X OPTIONS https://api.example.com/users \
       -H "Origin: https://app.example.com" \
       -H "Access-Control-Request-Method: POST"

Q2: プリフライトリクエストの発生条件 — いつOPTIONSが送られるのか

■ シンプルリクエスト vs プリフライトリクエスト:

シンプルリクエスト(プリフライトなし):
  以下の条件を全て満たす場合、プリフライトは発生しない

  ① メソッドが以下のいずれか:
     GET, HEAD, POST

  ② 自動設定されるヘッダー以外に、以下のヘッダーのみを使用:
     Accept
     Accept-Language
     Content-Language
     Content-Type(下記の値のみ)
       ・application/x-www-form-urlencoded
       ・multipart/form-data
       ・text/plain

  ③ リクエストにReadableStreamを使用していない

  ④ XMLHttpRequestでevent listenerを登録していない

プリフライトが発生するケース:

  ✓ カスタムヘッダーを使用(Authorization, X-Request-Id等)
  fetch('https://api.example.com/users', {
    headers: {
      'Authorization': 'Bearer token',  // ← プリフライト発生
    },
  });

  ✓ Content-Typeがapplication/json
  fetch('https://api.example.com/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',  // ← プリフライト発生
    },
    body: JSON.stringify({ name: 'Taro' }),
  });

  ✓ PUT, DELETE, PATCHメソッド
  fetch('https://api.example.com/users/123', {
    method: 'DELETE',  // ← プリフライト発生
  });

プリフライトの流れ:

  1. ブラウザがOPTIONSリクエストを送信:
     OPTIONS /api/users HTTP/1.1
     Origin: https://app.example.com
     Access-Control-Request-Method: POST
     Access-Control-Request-Headers: content-type, authorization

  2. サーバーが許可情報を返す:
     HTTP/1.1 204 No Content
     Access-Control-Allow-Origin: https://app.example.com
     Access-Control-Allow-Methods: GET, POST, PUT, DELETE
     Access-Control-Allow-Headers: content-type, authorization
     Access-Control-Max-Age: 86400

  3. ブラウザが実際のPOSTリクエストを送信
     POST /api/users HTTP/1.1
     Content-Type: application/json
     Authorization: Bearer token

プリフライトのキャッシュ:
  Access-Control-Max-Age: 86400(秒)
  → 24時間はプリフライトをスキップして直接リクエスト送信
  → ブラウザによって上限あり(Chromeは2時間)

Q3: credentialsモードの設定 — CookieやAuthorizationヘッダーを送る方法

■ credentials モードの種類:
モード挙動
omit認証情報を送信しない(デフォルト)
→ Cookieなし、Authorizationなし
same-origin同一オリジンの場合のみ送信
→ クロスオリジンでは送信しない
include常に送信(クロスオリジンでも)
→ CookieとAuthorizationを送信
→ サーバー側で特別な設定が必要
クライアント側の設定:

  fetch('https://api.example.com/users', {
    credentials: 'include',  // ← Cookie/Authorizationを送信
    headers: {
      'Authorization': 'Bearer token',
    },
  });

サーバー側の必須設定(credentials: 'include'の場合):

  ✓ Access-Control-Allow-Origin に具体的なオリジンを指定(*は不可)
  res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');

  ✓ Access-Control-Allow-Credentials: true を返す
  res.setHeader('Access-Control-Allow-Credentials', 'true');

  ✗ 間違った例(エラーになる):
  res.setHeader('Access-Control-Allow-Origin', '*');  // ← 不可
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  // エラー: "The value of the 'Access-Control-Allow-Origin' header
  //         must not be '*' when credentials mode is 'include'"

動的にオリジンを返す実装:

  // Express.js
  const allowedOrigins = [
    'https://app.example.com',
    'https://admin.example.com',
  ];

  app.use((req, res, next) => {
    const origin = req.headers.origin;
    if (allowedOrigins.includes(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin);
      res.setHeader('Access-Control-Allow-Credentials', 'true');
    }
    next();
  });

Cookieの設定(クロスオリジンで送信する場合):

  Set-Cookie: session_id=abc123;
    SameSite=None;   ← クロスサイトで送信を許可
    Secure;          ← HTTPS必須
    HttpOnly;        ← XSS対策
    Path=/;
    Domain=.example.com

  注意:
  → SameSite=None を使用する場合、Secure属性が必須
  → HTTP接続ではSameSite=Noneが機能しない
  → Chromeは2020年からSameSite=Laxがデフォルト

セキュリティ上の注意:
  → credentials: 'include' は CSRF攻撃のリスクを高める
  → CSRF対策(CSRFトークン)を必ず実装
  → 信頼できるオリジンのみを許可
  → Cookieには SameSite=Lax or Strict を推奨(可能な限り)

まとめ

概念 キーポイント
同一オリジンポリシー スキーム + ホスト + ポート が全て一致する場合のみ同一オリジン
CORS クロスオリジンリクエストを安全に許可する仕組み(サーバー側で制御)
シンプルリクエスト GET/HEAD/POST + 限定ヘッダー → プリフライトなし
プリフライトリクエスト OPTIONS → 許可確認 → 実際のリクエスト(カスタムヘッダー、PUT/DELETE等)
Credentials credentials: 'include' + Allow-Origin(*不可)+ Allow-Credentials: true
セキュリティ ワイルドカード(*)は最小限に、null オリジン拒否、Vary: Originで検証

キーポイント

  1. CORSはサーバー側で制御: ブラウザはクロスオリジンリクエストを自動的にブロックし、サーバーが明示的に許可した場合のみ通す。クライアント側(JavaScript)だけでCORSエラーは解決できない。

  2. プリフライトリクエスト(OPTIONS)の理解が鍵: カスタムヘッダー(Authorization等)やapplication/jsonを使う場合、ブラウザは実際のリクエスト前にOPTIONSリクエストを送信。サーバーは認証不要で204を返し、Access-Control-Allow-*ヘッダーで許可情報を通知する必要がある。

  3. credentials: 'include'は慎重に: Cookie/Authorizationを送る場合、Allow-Originにワイルドカード(*)は使用不可。具体的なオリジンを動的に返す実装にし、CSRF対策(CSRFトークン)を必ず組み合わせる。


次に読むべきガイド

  • TLS/SSL - HTTPS通信の暗号化、証明書、暗号スイートの仕組みを学ぶ
  • 認証方式 - OAuth 2.0、JWT、セッション管理などCredentials付きCORSに必要な認証の基礎を学ぶ
  • ネットワーク攻撃と対策 - CORS設定の不備を悪用する攻撃パターンと防御策を学ぶ

参考文献

  1. Fetch Living Standard. "CORS Protocol." WHATWG, 2024. https://fetch.spec.whatwg.org/#http-cors-protocol CORSの正式仕様。Same-Origin Policy、プリフライトリクエスト、 Credentialsモードの詳細を定義。

  2. MDN Web Docs. "Cross-Origin Resource Sharing (CORS)." Mozilla, 2024. https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS CORSの実践的ガイド。エラーメッセージの意味、設定例、 デバッグ方法を詳細に解説。

  3. web.dev. "Cross-Origin Resource Sharing (CORS)." Google, 2024. https://web.dev/cross-origin-resource-sharing/ Googleのベストプラクティス。セキュアなCORS設定、 パフォーマンス最適化(プリフライトキャッシュ)。

  4. OWASP. "CORS Security Cheat Sheet." OWASP, 2024. https://cheatsheetseries.owasp.org/cheatsheets/CORS_Security_Cheat_Sheet.html セキュリティ観点でのCORS設定。一般的な脆弱性、 安全な実装パターン、攻撃シナリオ。

  5. RFC 6454. "The Web Origin Concept." IETF, 2011. https://www.rfc-editor.org/rfc/rfc6454 オリジンの定義、Same-Origin Policyの正式仕様。 セキュリティ境界の概念を定義。

  6. "Understanding CORS and Dealing with CORS Errors in Angular." Bitovi, 2023. https://www.bitovi.com/blog/understanding-cors-and-dealing-with-cors-errors-in-angular 実装例とトラブルシューティング。Angular/React/Vue.jsでの CORS対応パターン。