CORSとは?オリジン間リソース共有の仕組みと設定方法を図解で解説

kento_morota 18分で読めます

「Access to fetch at '...' from origin '...' has been blocked by CORS policy」——フロントエンドとAPIを別々に開発していると、一度は遭遇するエラーメッセージです。

CORSエラーに対して、なんとなくヘッダーを追加して回避している方も多いのではないでしょうか。本記事では、CORSの仕組みを基礎から理解し、適切な設定を行えるようになることを目指します。

同一オリジンポリシーとは何か

CORSを理解するには、まず「同一オリジンポリシー(Same-Origin Policy)」を知る必要があります。これはWebセキュリティの根幹をなす仕組みです。

オリジンの定義

オリジンは「スキーム(プロトコル)」「ホスト(ドメイン)」「ポート番号」の3つの組み合わせで決まります。

// オリジンの構成要素
https://www.example.com:443/path/page
│       │                │
スキーム  ホスト           ポート

// 同一オリジンの判定例
https://example.com/page1  と  https://example.com/page2  → 同一オリジン ✅
https://example.com        と  http://example.com         → 異なるオリジン ❌(スキームが違う)
https://example.com        と  https://api.example.com    → 異なるオリジン ❌(ホストが違う)
https://example.com        と  https://example.com:8080   → 異なるオリジン ❌(ポートが違う)

同一オリジンポリシーの役割

同一オリジンポリシーは、あるオリジンのWebページが別のオリジンのリソースに無制限にアクセスすることを制限するブラウザのセキュリティ機構です。

この制限がないと、以下のような攻撃が容易に成立してしまいます。

攻撃シナリオの例
1. ユーザーが銀行サイト(bank.example.com)にログインしている
2. 攻撃者のサイト(evil.example.com)を訪問する
3. 攻撃者のJavaScriptがbank.example.comのAPIを呼び出す
4. ブラウザがCookieを自動送信し、ユーザーの口座情報が取得される

同一オリジンポリシーがあるため、手順3でブラウザがレスポンスの読み取りをブロックし、攻撃が成立しません。

制限されるものとされないもの

同一オリジンポリシーで制限されるもの

・fetch/XMLHttpRequestによるAPIリクエスト(レスポンスの読み取りがブロックされる)
・iframe内の別オリジンコンテンツへのDOM操作
・Canvas要素で別オリジンの画像データを読み取る操作

制限されないもの

<img>タグによる画像の読み込み
<link>タグによるCSSの読み込み
<script>タグによるJavaScriptの読み込み
<form>タグによるフォーム送信

CORSの基本的な仕組み

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

シンプルリクエスト

以下の条件をすべて満たすリクエストは「シンプルリクエスト」として扱われ、プリフライトリクエストなしで直接送信されます。

・メソッドがGET、HEAD、POSTのいずれか
・ヘッダーがAccept、Accept-Language、Content-Language、Content-Type(一部の値のみ)に限定される
・Content-Typeがapplication/x-www-form-urlencoded、multipart/form-data、text/plainのいずれか

// シンプルリクエストの流れ
// 1. ブラウザがリクエストを送信(Originヘッダーを自動付与)
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com

// 2. サーバーがCORSヘッダー付きでレスポンスを返す
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

{"data": "..."}

// 3. ブラウザがAccess-Control-Allow-Originを確認
//    - Originと一致すればレスポンスを許可
//    - 一致しなければCORSエラー

プリフライトリクエスト

シンプルリクエストの条件を満たさない場合、ブラウザはまず「プリフライトリクエスト」をOPTIONSメソッドで送信し、サーバーにリクエストが許可されるか確認します。

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

// ステップ1: OPTIONSリクエスト(プリフライト)
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
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: 本来のリクエスト(プリフライトが成功した場合のみ)
PUT /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOi...

{"name": "山田太郎"}

// ステップ4: サーバーのレスポンス
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com

Access-Control-Max-Ageヘッダーにより、プリフライトの結果をブラウザがキャッシュします。指定した秒数の間、同じリクエストのプリフライトは省略されます。

資格情報を含むリクエスト

Cookieや認証ヘッダーなどの資格情報をクロスオリジンリクエストに含めるには、追加の設定が必要です。

// クライアント側:credentials: 'include' を指定
fetch('https://api.example.com/data', {
  method: 'GET',
  credentials: 'include', // Cookieを送信
});

// サーバー側:必要なヘッダーを返す
Access-Control-Allow-Origin: https://app.example.com  // ワイルドカード(*) は不可
Access-Control-Allow-Credentials: true

重要な制約credentials: 'include'を使用する場合、Access-Control-Allow-Originにワイルドカード(*)は使用できません。具体的なオリジンを指定する必要があります。

Express.jsでのCORS設定

Node.js(Express)でのCORS設定方法を解説します。

corsパッケージを使用する方法(推奨)

const express = require('express');
const cors = require('cors');
const app = express();

// 基本的な設定
app.use(cors({
  origin: 'https://app.example.com', // 許可するオリジン
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400, // プリフライトキャッシュ:24時間
}));

// 複数オリジンの許可
const allowedOrigins = [
  'https://app.example.com',
  'https://admin.example.com',
  'http://localhost:3000', // 開発環境
];

app.use(cors({
  origin: (origin, callback) => {
    // originがundefinedの場合はサーバー間通信(許可する)
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('CORSポリシーによりブロックされました'));
    }
  },
  credentials: true,
}));

// 特定のルートのみCORSを許可
app.get('/api/public', cors({ origin: '*' }), (req, res) => {
  res.json({ message: '公開API' });
});

手動で設定する方法

// ミドルウェアで手動設定
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');
    res.setHeader('Vary', 'Origin'); // キャッシュの問題を防ぐ
  }

  if (req.method === 'OPTIONS') {
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.setHeader('Access-Control-Max-Age', '86400');
    return res.status(204).end();
  }

  next();
});

Nginx・Caddyでのリバースプロキシ設定

アプリケーションサーバーの前段にリバースプロキシを配置している場合、そこでCORSヘッダーを設定することもできます。

Nginxでの設定

# nginx.conf
server {
    listen 443 ssl;
    server_name api.example.com;

    location /api/ {
        # プリフライトリクエストの処理
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'https://app.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;
            add_header 'Content-Length' 0;
            return 204;
        }

        # 通常のリクエスト
        add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        add_header 'Vary' 'Origin' always;

        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Caddyでの設定

# Caddyfile
api.example.com {
    @cors_preflight method OPTIONS

    handle @cors_preflight {
        header Access-Control-Allow-Origin "https://app.example.com"
        header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
        header Access-Control-Allow-Headers "Content-Type, Authorization"
        header Access-Control-Max-Age "86400"
        respond "" 204
    }

    header Access-Control-Allow-Origin "https://app.example.com"
    header Access-Control-Allow-Credentials "true"
    header Vary "Origin"

    reverse_proxy localhost:3000
}

よくあるCORSエラーと解決方法

開発中に遭遇しやすいCORSエラーとその解決方法をまとめます。

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

// エラーメッセージ
// No 'Access-Control-Allow-Origin' header is present on the requested resource

// 原因:サーバーがCORSヘッダーを返していない
// 解決方法:サーバーにAccess-Control-Allow-Originヘッダーを設定する

エラー2:ワイルドカードとcredentialsの併用

// エラーメッセージ
// The value of the 'Access-Control-Allow-Origin' header must not be
// the wildcard '*' when the request's credentials mode is 'include'

// 原因:credentials: 'include'を使いながらOriginに*を指定している
// 解決方法:具体的なオリジンを指定する
// NG: Access-Control-Allow-Origin: *
// OK: Access-Control-Allow-Origin: https://app.example.com

エラー3:プリフライトリクエストの失敗

// エラーメッセージ
// Response to preflight request doesn't pass access control check

// 原因:OPTIONSリクエストに対する応答が適切でない
// 解決方法:
// 1. OPTIONSメソッドへの応答を実装する
// 2. 必要なAccess-Control-Allow-Methodsを返す
// 3. 必要なAccess-Control-Allow-Headersを返す

エラー4:Varyヘッダーの不足によるキャッシュ問題

// 原因:CDNやブラウザがCORSレスポンスをキャッシュし、
//       異なるオリジンに対して誤ったレスポンスを返してしまう

// 解決方法:Varyヘッダーを必ず設定する
res.setHeader('Vary', 'Origin');
// これにより、Originごとに異なるキャッシュが保持される

CORS設定のセキュリティベストプラクティス

CORSの設定を誤ると、同一オリジンポリシーの保護を無効化してしまう危険があります。以下のベストプラクティスを守りましょう。

1. ワイルドカード(*)の使用を最小限にする
公開APIなど、誰からのアクセスも許可すべき場合のみ使用してください。認証が必要なエンドポイントでは絶対にワイルドカードを使用しないでください。

2. Originの検証にはホワイトリストを使う
正規表現でのマッチングは避けてください。/example\.com$/のようなパターンは、evil-example.comにもマッチしてしまいます。

// 悪い例:正規表現でのマッチング
if (/example\.com$/.test(origin)) { // evil-example.comも通過してしまう
  res.setHeader('Access-Control-Allow-Origin', origin);
}

// 良い例:完全一致のホワイトリスト
const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
if (allowedOrigins.includes(origin)) {
  res.setHeader('Access-Control-Allow-Origin', origin);
}

3. 不要なメソッド・ヘッダーを許可しない
必要最小限のメソッドとヘッダーのみを許可してください。

4. Access-Control-Max-Ageを適切に設定する
プリフライトリクエストの頻度を減らしつつ、設定変更が反映されるまでの時間も考慮してください。一般的に24時間(86400秒)が推奨値です。

5. エラーレスポンスにもCORSヘッダーを付与する
alwaysパラメータ(Nginx)やミドルウェアの配置順(Express)に注意し、エラーレスポンスでもCORSヘッダーが返されるようにしてください。

まとめ:CORSを正しく理解して設定しよう

CORSは、Webのセキュリティモデルの重要な構成要素です。エラーが出たから適当にヘッダーを追加するのではなく、仕組みを理解した上で適切に設定することが大切です。

本記事のポイントを振り返ります。

基本の理解
・同一オリジンポリシーはブラウザのセキュリティ機構
・CORSはその制限を安全に緩和する仕組み
・オリジンはスキーム・ホスト・ポートの組み合わせで決まる

リクエストの種類
・シンプルリクエスト:条件を満たせば直接送信される
・プリフライトリクエスト:OPTIONSメソッドで事前確認が行われる
・資格情報付きリクエスト:ワイルドカードが使用できない

セキュリティ対策
・ワイルドカードの使用は最小限にする
・Originの検証は完全一致のホワイトリストで行う
・Varyヘッダーでキャッシュの問題を防ぐ

CORSエラーに遭遇したら、まずブラウザの開発者ツールでリクエスト・レスポンスヘッダーを確認しましょう。エラーメッセージの内容を正しく読み取れば、ほとんどの問題は解決できます。

#CORS#セキュリティ#Web
共有:
無料メルマガ

週1回、最新の技術記事をお届け

AI・クラウド・開発の最新記事を毎週月曜にメールでお届けします。登録は無料、いつでも解除できます。

プライバシーポリシーに基づき管理します

AI活用のヒントをお探しですか?お気軽にご相談ください。

まずは話だけ聞いてもらう