CORS入門: 同一生成元ポリシーを補完するクロスオリジンリクエストの実装とセキュリティ対策

はじめに — CORS(クロスオリジンリソースシェアリング)とは何か

CORS(Cross-Origin Resource Sharing、クロスオリジンリソースシェアリング)は、Web ブラウザが実装するセキュリティ機構「同一生成元ポリシー(Same-Origin Policy)」を補完するための仕組みです。異なるオリジン(origin:スキーム、ホスト名、ポートの組合せが異なるもの)間で、安全にリソースを共有するために、サーバ側が HTTP レスポンスヘッダを介してアクセスを制御します。CORS はブラウザ側で強制されるもので、サーバは適切なヘッダを返すことでブラウザにアクセス許可を伝えます。

同一生成元ポリシー(Same-Origin Policy)のおさらい

同一生成元ポリシーは、ある Web ページ(例:https://example.com)が読み込んだスクリプトが、別の生成元(例:https://api.example.net や http://example.com:8080)上のデータへ制限なくアクセスすることを防ぎます。ここでの「生成元(origin)」は、スキーム(http/https)、ホスト、ポートの組合せで定義されます。これはクロスサイトスクリプティング(XSS)や機密情報の漏洩を防ぐ基本的な制約です。

CORS の基本的な仕組み

CORS のコアは「ブラウザがリクエスト時に Origin ヘッダを送る」「サーバは Access-Control-... のヘッダで応答する」というやり取りです。ブラウザはサーバの応答ヘッダを見て、JavaScript にリソースアクセスを許可するかどうかを判断します。サーバが何らかの Access-Control-Allow-* ヘッダを返さない場合、ブラウザはレスポンスを JavaScript に渡さず、コンソールにエラーを出力します(サーバ側では通常 403 ではなく 200 が返っていてもブラウザでブロックされます)。

「シンプルリクエスト」と「プレフライト(事前確認)リクエスト」

CORS リクエストは大きく二つに分かれます。

  • シンプルリクエスト(Simple Request) — ブラウザは通常の HTTP リクエストを送り、サーバの応答ヘッダに Access-Control-Allow-Origin があれば許可されます。シンプル扱いされる条件には、HTTP メソッドが GET/HEAD/POST のいずれかであること、Content-Type ヘッダが限られた値(application/x-www-form-urlencoded、multipart/form-data、text/plain)に限定されること、カスタムヘッダを付けないことなどがあります。

  • プレフライト(Preflight)リクエスト — 実際のリクエストの前にブラウザが自動的に OPTIONS メソッドで「これからこういうメソッド・ヘッダでリクエストするが良いか?」と確認を送ります。プレフライトは、PUT/DELETE などのメソッドや、カスタムヘッダや特殊な Content-Type を使う場合に発生します。プレフライトのリクエストには Access-Control-Request-Method や Access-Control-Request-Headers が含まれ、サーバは Access-Control-Allow-Methods と Access-Control-Allow-Headers で応答します。

主要な CORS レスポンスヘッダ

  • Access-Control-Allow-Origin — 許可するオリジンを指定。ワイルドカード「*」を使うと広く許可するが、資格情報付きリクエスト(クッキーや Authorization ヘッダ)を受け付ける場合は使えない(ブラウザは拒否)。

  • Access-Control-Allow-Methods — プレフライトへの応答で、許可する HTTP メソッド(例:"GET, POST, OPTIONS")を列挙。

  • Access-Control-Allow-Headers — プレフライトへの応答で、送信が許可されるカスタムヘッダを列挙。

  • Access-Control-Allow-Credentials — "true" を返すと、ブラウザは資格情報(Cookie、HTTP 認証情報、クライアント証明書)を伴うクロスオリジンリクエストを許可。ただし、この場合 Access-Control-Allow-Origin にワイルドカードは使えず、明示的なオリジンを返す必要がある。

  • Access-Control-Expose-Headers — JavaScript 側から取得可能にするレスポンスヘッダを列挙。デフォルトで露出されるのは Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma などの「セーフな」ヘッダのみ。

  • Access-Control-Max-Age — プレフライト応答の結果をブラウザがキャッシュしてよい秒数を指定。なおブラウザ側で上限が設けられることがあります。

  • Vary: Origin — サーバが Origin を見て応答を切り替える場合は、キャッシュのために Vary: Origin を付けることが推奨されます。これを付けないと、CDN 等が誤ったレスポンスを返す恐れがあります。

ブラウザとサーバの責任範囲

重要な点は、CORS は「ブラウザ側で強制される制約」であり、サーバが何らかのレスポンスを返しても、ブラウザがそれを JavaScript に渡さないことがある点です。サーバ自体は任意のオリジンからのリクエストを処理することができますが、ブラウザがアクセスを遮断するとフロントエンドはそのレスポンスを利用できません。また、CORS 設定を甘くすると(例えば任意のオリジンを許可するなど)本来サーバ側で保護すべき認可ロジックが無効化されるわけではないが、意図しないドメインからの利用を許してしまいリスクが増します。

よくある落とし穴と誤解

  • 「サーバで CORS を有効にすれば安全」ではない — CORS はブラウザの同一生成元ポリシーを緩和する仕組みであり、サーバ側で許可したからといってそのリクエストの正当性を保証するものではありません。CSRF 対策や認可チェックは別途必要です。

  • ワイルドカード(*)と資格情報 — Access-Control-Allow-Origin: * と Access-Control-Allow-Credentials: true は同時に使えません。ブラウザはこれを拒否します。

  • プリフライトはブラウザが自動で行う — 開発者が明示的に OPTIONS を送る必要はありませんが、サーバは OPTIONS に対して適切なレスポンスを返す必要があります。

  • レスポンスヘッダの露出 — カスタムレスポンスヘッダを JavaScript から読みたい場合は Access-Control-Expose-Headers を設定する必要があります。

  • エラーメッセージ — ブラウザのコンソールに出る「No 'Access-Control-Allow-Origin' header is present on the requested resource.」などは、サーバが適切なヘッダを返していないことを示していますが、サーバ側のログには通常その理由は出ず、サーバは正常な 200 を返していることもあります。

実装例(サーバ側)

ここでは代表的な実装例を簡単に示します。

Node.js(Express)例:

// npm install cors
const express = require('express');
const cors = require('cors');
const app = express();

// 指定オリジンのみ許可する例
app.use(cors({
  origin: 'https://example.com',
  credentials: true
}));

app.get('/api/data', (req, res) => {
  res.json({ hello: 'world' });
});

app.listen(3000);

Nginx(静的レスポンスやリバースプロキシで OPTIONS を処理):

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

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

セキュリティの観点とベストプラクティス

  • 必要最小限のオリジンのみを許可する(ワイルドカードの多用は避ける)。
  • 資格情報を扱う場合は必ず明示的なオリジンを返す(ワイルドカード不可)。
  • プレフライトで許可するメソッドとヘッダは最小限に限定する。
  • サーバ側では常に適切な認可・認証を行う(CORS はこれらの代替ではない)。
  • Origin ヘッダを鵜呑みにしない — サーバが信頼するドメイン一覧(ホワイトリスト)と照合する。
  • キャッシュ(Access-Control-Max-Age)や Vary: Origin を適切に設定して、CDN 経由の誤配信を防ぐ。

デバッグのヒント

  • ブラウザ開発者ツールのネットワークタブでリクエストとレスポンスヘッダを確認する。特に Origin と Access-Control-* 系ヘッダをチェック。
  • 「No 'Access-Control-Allow-Origin' header is present…」などのコンソールメッセージを手がかりにする。
  • プレフライトが送られているか(OPTIONS)を確認する。サーバが 200/204 を返しているか、必要な Access-Control-Allow-... ヘッダを返しているかを確認。
  • 資格情報を使う場合は、フロントエンドで fetch/axios の credentials / withCredentials 設定が正しいかをチェックする。

まとめ

CORS は、Web アプリケーションが安全にクロスオリジンのリソースを利用できるようにするための標準的で強力な仕組みです。ただし、その効果を正しく得るためにはブラウザとサーバの役割を理解し、適切にヘッダを設定する必要があります。特に資格情報やカスタムヘッダを扱う場合はプレフライトやレスポンス設定に注意が必要で、過度に広い許可設定(ワイルドカードの乱用など)はセキュリティリスクにつながります。設計時にはオリジンのホワイトリスト、最小権限の原則、サーバ側の認可チェックを併せて行うことが重要です。

参考文献