現場のエンジニア諸君、今日も泥臭いデバッグと戦っていることだろう。
セキュリティは「魔法の杖」じゃない。地味な設定の積み重ねが、数億円の損害とブランドの失墜を救う盾になる。今回は、API開発で誰もが一度は悩み、そして安易に逃げてしまう「CORS(Cross-Origin Resource Sharing)」の深淵について話そう。
「とりあえず Access-Control-Allow-Origin: * にしておけば動くから」という考え、今すぐ捨ててくれ。それは、家の玄関に「誰でも自由に入っていいですよ」と看板を掲げるのと同じだ。
—
CORSが狙われる理由:ブラウザという名の「共犯者」
CORSは本来、ブラウザが持つ「同一生成元ポリシー(Same-Origin Policy)」を緩和するための仕組みだ。しかし、ここを正しく設定しないと、攻撃者はユーザーのブラウザを悪用し、「そのユーザーしかアクセスできないはずのAPI」を勝手に叩くことが可能になる。
攻撃シナリオ:CSRFと情報漏洩の融合
攻撃者の罠サイトに被害者がアクセスしたとする。被害者がログイン済みの管理画面APIに対し、攻撃者のスクリプトが非同期リクエストを投げる。もしサーバーが Access-Control-Allow-Origin: * を許可していれば、ブラウザは「おっ、許可されてるな」と勘違いし、Cookieや認証トークンを勝手に付与してリクエストを送ってしまう。
結果、機密情報が攻撃者のサーバーへと送信される。これがCORS設定不備のリアルな脅威だ。
—
避けるべき「悪魔のコード」
以下の設定は、運用上もっとも多い「思考停止」の産物だ。
# 決してやってはいけない設定
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
* を指定しつつ Allow-Credentials を有効にするのは、セキュリティ的に自殺行為だ。ブラウザはこれを検知してエラーを出すはずだが、設定の組み合わせ次第で予期せぬ挙動を招く。
—
実践:セキュアな実装コード(PHPの場合)
APIサーバー側では、接続元をホワイトリストで管理し、動的にレスポンスヘッダーを制御するのが鉄則だ。
<?php
// 許可されたオリジンのリスト
$allowed_origins = [
'https://app.example.com',
'https://dashboard.example.com'
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
// オリジンが許可リストに含まれているかチェック
if (in_array($origin, $allowed_origins)) {
header("Access-Control-Allow-Origin: " . $origin);
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
// 資格情報(Cookie等)を許可する場合
header("Access-Control-Allow-Credentials: true");
}
// プリフライトリクエスト(OPTIONS)への対応
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
?>
重要なポイント
1. ホワイトリスト運用: * は絶対に使わない。必ずドメインを明示的に指定する。
2. プリフライトの処理: ブラウザは複雑なリクエストの前に OPTIONS メソッドで確認を行う。ここで適切に 200 OK を返さないと、アプリは正しく動作しない。
—
インフラ・Nginxでの防御的設定
アプリケーション層だけでなく、インフラ側でも防御を固めるのがプロの仕事だ。Nginxで制御する場合は以下のようになる。
location /api/ {
# オリジンの動的検証
set $cors_origin "";
if ($http_origin ~* "^https?://(app|dashboard)\.example\.com$") {
set $cors_origin $http_origin;
}
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
# プリフライトの即時終了
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin;
add_header 'Access-Control-Max-Age' 86400; # キャッシュ時間を設定
return 204;
}
}
Access-Control-Max-Age を適切に設定することで、ブラウザからサーバーへの無駄な OPTIONS リクエストを減らし、パフォーマンス向上と負荷軽減の両立が図れる。
—
最後に:セキュリティは「性悪説」で考えろ
今回紹介した実装は、いわば「基本のキ」だ。現場では、サブドメインの複雑な運用や、特定の認証フローとの兼ね合いで壁にぶつかることもあるだろう。
もし、設定が正しく動いているか不安になったら、ブラウザの「開発者ツール」の「ネットワーク」タブを覗いてくれ。レスポンスヘッダーに意図した Access-Control-Allow-Origin が入っているか、OPTIONS リクエストが正しく処理されているか。泥臭く確認することこそ、最強の防御だ。
「面倒だから」という理由でセキュリティを妥協するな。コードの一行、設定の一箇所が、君たちのプロダクトを守る最後の砦になることを忘れないでほしい。
健闘を祈る。
コメント