CORS Security
CORS(Cross-Origin Resource Sharing、クロスオリジンリソース共有)は、最新の ブラウザによって実装されているセキュリティメカニズムであり、Web アプリケーションが元のページを 配信したドメインとは異なるドメインへ HTTP リクエストを行う方法を制御し、 Same-Origin Policy (SOP) を制御された形で緩和します。SOP は、 あるオリジンのスクリプトが別のオリジンのリソースにアクセスすることを制限する 基本的なセキュリティポリシーです。CORS は、フロントエンドが異なるドメインで ホストされた API を利用する必要が頻繁にある最新の Web アプリケーションアーキテクチャにとって 不可欠ですが、その不適切な設定は、現代の Web アプリケーションにおいて 最も一般的で危険な脆弱性の 1 つとなっています。 CORS の設定ミスは、機密データを許可されていないドメインに公開し、 アンチ CSRF トークンが存在していてもクロスサイトリクエストフォージェリ (CSRF) 攻撃を可能にし、 認証情報の窃取を助長し、極端なケースでは、認証済みユーザーになりすまして攻撃者が 特権的な操作を実行できるようにする可能性があります。この問題は、多くの 開発者が開発中に CORS エラーに遭遇した際、セキュリティ上の影響を十分に理解しないまま、 過度に寛容な解決策(ワイルドカード "*" の使用や、リクエストのオリジンを自動的に反映させるなど)を 選択するという事実によって悪化します。本記事では、CORS の基礎、 一般的な設定の脆弱性を詳しく掘り下げ、さまざまなプラットフォームやフレームワークにわたる 安全な実装のための堅牢なプラクティスを確立し、機能性と適切な防御的姿勢のバランスを取ります。
Same-Origin Policy (SOP)
ブラウザは SOP を実装しています。スクリプトは同一オリジン (プロトコル + ドメイン + ポート)のリソースにのみアクセスできます。CORS は SOP を制御された形で緩和します。
# 同一オリジン
https://example.com/api ← https://example.com/app [OK]
# 異なるオリジン(SOP によりブロック)
https://example.com ← http://example.com (プロトコル)
https://example.com ← https://api.example.com (サブドメイン)
https://example.com ← https://example.com:8080 (ポート)
CORS Headers
Access-Control-Allow-Origin
# 特定のオリジンを許可(推奨)
Access-Control-Allow-Origin: https://trusted.com
# 任意のオリジンを許可(危険!)
Access-Control-Allow-Origin: *
# ホワイトリストに基づく動的設定(正しい)
const allowedOrigins = ['https://app1.com', 'https://app2.com'];
const origin = request.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
その他の重要な Headers
# 認証情報を許可(cookies、auth headers)
Access-Control-Allow-Credentials: true
# 許可する HTTP メソッド
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
# リクエストで許可する headers
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With
# クライアントサイド JavaScript に公開する headers
Access-Control-Expose-Headers: X-Custom-Header, X-Request-Id
# プリフライトのキャッシュ時間(秒)
Access-Control-Max-Age: 86400
Preflight Requests
ブラウザは「単純でない」リクエストの前に OPTIONS リクエストを送信して権限を確認します。
# クライアントがプリフライトを送信
OPTIONS /api/resource HTTP/1.1
Origin: https://app.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization
# サーバーが権限を返答
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization
Access-Control-Max-Age: 86400
一般的な CORS の脆弱性
1. 認証情報を伴うワイルドカード
# [エラー] 脆弱 - 機能せず危険
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
# ブラウザはこの組み合わせをブロックします
# [OK] 正しい - 認証情報を伴う特定のオリジン
Access-Control-Allow-Origin: https://trusted.com
Access-Control-Allow-Credentials: true
2. Reflection Attack
# [エラー] 脆弱 - 任意の origin を反映
const origin = request.headers.origin;
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
# [OK] 正しい - ホワイトリスト検証
const allowedOrigins = ['https://app.com', 'https://admin.com'];
const origin = request.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
3. Subdomain Wildcard
# [エラー] 脆弱 - 不適切に実装された regex
const origin = request.headers.origin;
if (/https:\/\/.*\.example\.com/.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
// https://evil.example.com.attacker.com を受け入れてしまう
# [OK] 正しい - 厳格な検証
const origin = request.headers.origin;
if (/^https:\/\/[a-z0-9-]+\.example\.com$/.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
技術別の安全な設定
Node.js/Express (CORS middleware)
const cors = require('cors');
// 安全な設定
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com'
];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400
};
app.use(cors(corsOptions));
Nginx
# 条件付き設定
map $http_origin $cors_origin {
default "";
"~^https://app\\.example\\.com$" $http_origin;
"~^https://admin\\.example\\.com$" $http_origin;
}
server {
location /api {
if ($cors_origin != "") {
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" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
}
if ($request_method = OPTIONS) {
return 204;
}
}
}
Apache
# .htaccess
SetEnvIf Origin "^https://(app|admin)\\.example\\.com$" CORS_ORIGIN=$0
Header always set Access-Control-Allow-Origin "%e" env=CORS_ORIGIN
Header always set Access-Control-Allow-Credentials "true" env=CORS_ORIGIN
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE" env=CORS_ORIGIN
Header always set Access-Control-Allow-Headers "Authorization, Content-Type" env=CORS_ORIGIN
# OPTIONS プリフライトに応答
RewriteEngine On
RewriteCond % OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
Testing CORS
# curl でのテスト
curl -H "Origin: https://evil.com" \\
-H "Access-Control-Request-Method: DELETE" \\
-H "Access-Control-Request-Headers: Authorization" \\
-X OPTIONS \\
https://api.example.com/resource
# JavaScript テスト
fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
}).then(response => console.log(response));
Best Practices
- 機密データを扱う API では決してワイルドカード (*) を使用しない
- 明示的なホワイトリストで許可するオリジンを指定する
- 厳格な検証を安全な regex でオリジンに対して行う
- credentials を最小限に:本当に必要な場合のみ有効にする
- Least privilege(最小権限):必要なメソッドと headers のみを許可する
- Cache preflight(プリフライトのキャッシュ):Max-Age を使用してオーバーヘッドを削減する
- Monitoring(監視):疑わしい CORS の拒否をログに記録する
安全な CORS チェックリスト
- [OK] オリジンが明示的なホワイトリストに対して検証されている
- [OK] 検証用 regex がバイパスを許可しない
- [OK] Credentials は必要な場合にのみ有効化されている
- [OK] メソッドと headers が最小限に制限されている
- [OK] プリフライトが正しく設定されている
- [OK] 悪意のあるオリジンに対してテスト済み
- [OK] 拒否されたリクエストのログが監視されている
