この記事は、AI製品のセキュリティ実装記録シリーズの1本である。子ども向けAI英語教材「Puku」で、ブラウザから直接呼び出される会員向けAPIのCORS(オリジン間リソース共有)設定を見直した2026年9月の記録をまとめる。認証コードの試行回数を制限した記事と同じ監査で見つかった項目である。
この記事で分かること
- CORSのpreflight処理が複数存在する構成で、何が起きうるか
- 2026年9月にどう直したか
この記事で分からないこと(正直に書く)
- 見直し前の設定を実際に外部から突かれていたかどうか(一次資料には構成上の不備の記載のみで、外部からの悪用記録はない)
何を作っていたか
会員向けAPIには、共通のエンドポイント全体に対するpreflight(事前確認)処理と、会員機能専用のルートに対するpreflight処理という、2つのCORS処理が存在していた。会員機能のUIを本番に配線し、ブラウザから直接APIを呼ぶ構成を整える中で、このCORS設定全体を見直した(出所: 自社の運用記録、確認日2026-09-06)。
どこに穴があったか
2つのpreflight処理が存在する構成では、評価される順番によって結果が変わる。会員機能専用の許可リストより先に共通の緩い処理が評価される並びだと、許可リストを用意していても実際には機能せず、想定外のオリジンからのリクエストも許可されうる応答が先に返る状態になり得た(出所: 自社の運用記録、確認日2026-09-06)。処理を追加しただけでは足りず、どちらが先に評価されるかという並び順自体が正しさを左右していた。
どう直したか(2026年9月)
会員機能のルートに限って、正規に使うオリジン(本番アプリのドメインと、デプロイのたびに生成されるプレビュー環境のドメインパターンなど)だけを許可リストとして明示し、それ以外のオリジンからのリクエストは許可しない設定にした。あわせて会員機能専用のpreflight処理を共通処理より先に評価される並びに変更し、レスポンスにVary: Originヘッダーを付けた(出所: 自社の運用記録、確認日2026-09-06)。
MDNのCORS解説では、Cookieなど認証情報を伴うリクエストにはAccess-Control-Allow-Originにワイルドカード(*)を使えず、具体的なオリジンを指定する必要があるとされている。またオリジンによって応答内容が変わるリソースの例として、レスポンスにVary: Originを付ける形も示されている。今回の許可リスト化とVary: Originの追加はこの方向に沿う(出所: MDN Web Docs「Cross-Origin Resource Sharing (CORS)」https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS、確認日2026-09-06)。
同じ穴を持つ製品の見分け方
開発者ツールのNetworkタブで、APIレスポンスのAccess-Control-Allow-Originヘッダーの値を確認するのが手早い。Cookieなど認証情報を使うAPIなのに値が*になっていれば、仕様上も成立しない組み合わせであり要注意である。加えて、普段使わない適当なオリジンから実際にリクエストを送り、ブロックされずに応答が返るかも見分け方になる。複数のpreflight処理が併存する構成では、並び順そのものも確認しておきたい。
コードそのものを応答に含めていた不備は最初の記事、公開基盤側のレート制限はIP単位のレート制限の記事、社内向けの監査ログ設計は社内文書RAGの監査ログ設計の記事を参照してほしい。
自社製品のAPI設計・CORS設定について相談したい場合は、お問い合わせから編集部まで連絡してほしい。
よくある質問
Q1. CORSのpreflight処理が複数あると、具体的に何が問題になるのか
評価順によって結果が変わる。専用の許可リストより先に共通の緩い処理が評価される並びだと、許可リストを用意していても機能せず、想定外のオリジンからのリクエストが通りうる。
Q2. 2026年9月にどう直したのか
会員機能のルートに限って正規のオリジンだけを許可リストにし、会員機能専用のpreflight処理を共通処理より先に評価される並びに変更した。あわせてレスポンスにVary: Originヘッダーを付けた。
Q3. Access-Control-Allow-Originが*になっていれば、それだけで問題なのか
認証情報を伴わないリクエストであれば仕様上は成立しうる。ただしCookieなど認証情報を使うAPIで*になっている場合は、仕様上も本来成立しない組み合わせであり、確認が必要である。
Q4. Vary: Originを付ける理由は何か
レスポンスの内容がリクエスト元のオリジンによって変わることをキャッシュ層に伝えるためである。付けていないと、あるオリジン向けの応答が別のオリジンにキャッシュ経由で返ってしまう可能性がある。