ssecutils
Security

CORS と Same-Origin Policy

著者 aki公開 更新 9

「No 'Access-Control-Allow-Origin' header」エラーで詰まった人へ

フロントエンド開発で API を叩いた瞬間、コンソールに見覚えのある赤文字:

Access to fetch at 'https://api.example.com/users' from origin
'https://app.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

この記事は、上記のエラーを「とりあえず動かす」ではなく「なぜ起きるのか」「どう直すのが正解か」を理解するためのものです。仕組みを理解すれば、CORS は単なる障害物ではなく Web の安全を支える重要な防壁だと分かります。

そもそも Same-Origin Policy(同一オリジンポリシー)とは

ブラウザには Same-Origin Policy(SOP)という基本ルールがあり、「あるオリジンの JavaScript は、別オリジンのリソースに自由にアクセスできない」という制限がかかっています。1995 年の Netscape Navigator 2.0 に導入されて以来、ブラウザ内セキュリティの土台です。

オリジンの定義はシンプルで、スキーム + ホスト + ポートの3つ組:

URLオリジン同一?
https://example.com/foohttps://example.com:443基準
https://example.com/barhttps://example.com:443同じ ✅
http://example.com/foohttp://example.com:80違う ❌(スキーム違い)
https://api.example.comhttps://api.example.com:443違う ❌(ホスト違い)
https://example.com:8080https://example.com:8080違う ❌(ポート違い)

「サブドメインが違うだけでも別オリジン」という点が重要。app.example.com から api.example.com を叩く時、別オリジン扱いで CORS が必要になります。

SOP がなぜ存在するのか

SOP の目的は2つの攻撃を防ぐこと:

1. クッキー窃取(Cookie 経由のなりすまし)

SOP がないと、罠サイト evil.com から fetch("https://bank.example/account") を実行すると、ブラウザは Cookie を律儀に付けて送信し、レスポンスをスクリプトから読めてしまう。残高がそのまま盗まれます。

SOP は「リクエストは送れるが、レスポンスは別オリジンの JS から読めない」という制限で、これを防ぎます。リクエスト送信自体は防げない点が CSRF の存在理由です。

2. DOM 越しの情報漏洩

罠サイトが iframe で銀行サイトを埋め込み、JavaScript で iframe の中身を読む、という攻撃も SOP で防がれます。iframe.contentDocument へのアクセスはオリジンが違うと例外になります。

SOP は何を「防がない」か

ここを誤解すると CORS の意味も曖昧になります。SOP は「JS からのレスポンス読み取り」を防ぐだけで、以下は全て可能です:

  • <img src="https://other.com/..."> で別オリジンの画像表示
  • <script src="https://cdn.example/lib.js"> で別オリジンの JS 読み込み(読み込んだスクリプトは自オリジンで動くので注意)
  • <form action="https://other.com/submit" method="POST"> で別オリジンへの POST 送信
  • fetchXMLHttpRequest でのリクエスト送信自体(レスポンスが読めないだけ)

最後の項目が大事。リクエストは飛んでサーバ側で副作用が起きる。これが CSRF の根本原理です。

では CORS とは何か

CORS(Cross-Origin Resource Sharing)は、SOP の「別オリジンレスポンスを JS から読めない」制限に「サーバ側が許可した場合のみ穴を開ける」仕組みです。2014 年に W3C で標準化されました。

CORS の本質はシンプルで、レスポンスヘッダ Access-Control-Allow-Origin でサーバが「このオリジンには見せていいよ」と宣言すること。

# クライアント(app.example.com)からのリクエスト
GET /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com

# サーバのレスポンス(CORS ヘッダ付き)
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

{"users": [...]}

ブラウザはレスポンスを受け取ると、Access-Control-Allow-Origin ヘッダの値が自分のオリジンと一致するかチェック。一致すれば JS にレスポンスを渡し、不一致なら CORS エラーで弾く。判定はブラウザ側で、サーバはあくまで「許可しますよ」と宣言するだけです。

2 種類のリクエスト: 単純とプリフライト

CORS リクエストは「単純リクエスト(Simple Request)」と「プリフライトを伴うリクエスト」の2種類に分かれます。

単純リクエスト(プリフライト不要)

以下をすべて満たす場合、ブラウザはいきなり本リクエストを送ります:

  • メソッドが GET / HEAD / POST
  • カスタムヘッダ無し(許可されているのは Accept, Accept-Language, Content-Language, Content-Type 等の限定リスト)
  • Content-Typeapplication/x-www-form-urlencoded / multipart/form-data / text/plain のいずれか

昔ながらの <form> から送れるリクエストは大体ここに該当します。レガシー HTML での挙動と完全互換にするための設計。

プリフライト(Preflight)

単純リクエストの条件を満たさない場合、ブラウザは本リクエストを送る前に「OPTIONS で許可確認」を入れます。これが「プリフライト」。

# プリフライトリクエスト
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization

# プリフライトレスポンス
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

OPTIONS で OK が返ったら、ブラウザは本リクエスト(PUT)を送信。OPTIONS が失敗したら本リクエストは送られないので、サーバ側に副作用は起きません。これがプリフライトの安全性。

典型的にプリフライトが発生するケース:

  • fetch(url, { method: "PUT" })DELETE
  • Authorization: Bearer ... を付けたリクエスト(カスタムヘッダ扱い)
  • Content-Type: application/json での POST

モダン SPA で API を叩くと、ほぼ必ずプリフライトが発生します。Network タブで OPTIONS リクエストを見たことがあるはず。

credentials(Cookie 付きリクエスト)

既定では CORS リクエストに Cookie は付きません。Cookie を送りたければ、クライアント側とサーバ側の両方で明示する必要があります。

// クライアント
fetch("https://api.example.com/me", {
  credentials: "include"
})
# サーバ
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

重要な制約:Access-Control-Allow-Credentials: trueAccess-Control-Allow-Origin: * は併用できない。Cookie 付きを許可するなら、Origin は具体値で返す必要があります。

# ❌ ダメ(ブラウザが拒否)
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

# ✅ OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

この仕様は意図的で、「あらゆるサイトから Cookie 付きで叩ける状態は危険すぎる」というブラウザベンダーの判断。

実体験:CORS エラーに初めてぶつかった話

個人開発でフロントエンドと API を別ポートで動かしていたときに、実際にこのエラーに遭遇したことがあります。最初は API 側のコードに問題があると思い込み、レスポンスやリクエストの中身ばかり確認していました。しかしブラウザのコンソールには CORS に関するエラーが出ていて、「API は正常に返しているのに、なぜブラウザから使えないんだろう」とかなり迷いました。

調べて Same-Origin Policy の存在を知り、原因がようやく理解できました。個人的には、CORS を「通信できない仕組み」だと思い込んでいたのですが、実際には「サーバーには届いているけれど、ブラウザがレスポンスを JavaScript に渡してくれない」というケースがあることを知ったのが、最初の大きな学びでした。

Access-Control-Allow-Origin: * も実際に試したことがあります。「CORS エラーを消したい」というだけの動機で設定したところ、それで動くようになり、一瞬「解決した」と思いました。しかし調べていくうちに、認証情報を含むリクエストでは * が使えないこと、そもそも「どこからのアクセスでも許可する」設定なので認証が必要な API には使えないことが分かりました。それ以来、CORS は「エラーを消すための設定」ではなく「どのオリジンにどこまでアクセスを許可するかを決める仕組み」だと考えるようになりました。

このとき遭遇した「* だと動かないケースがある」という制約は、後述するcredentials(Cookie 付きリクエスト)の項目で説明している Access-Control-Allow-Credentials: trueAccess-Control-Allow-Origin: * の併用不可、そのものでした。当時は仕様を知らずに手探りで試していましたが、後から仕組みを理解して「あのとき動かなかったのはこれだったのか」とつながりました。

よくある罠

罠1: ワイルドカード * の落とし穴

「とりあえず Access-Control-Allow-Origin: * で動いた」とよく言われます。確かに動きますが:

  • Cookie 付きリクエスト(credentials)は使えない
  • 認証 API では実用にならない
  • 「全世界に公開していい API」だけに限定すべき

本番環境では許可するオリジンを動的にチェックするのが普通:

// 例: Express
app.use((req, res, next) => {
  const allowed = ["https://app.example.com", "https://staging.example.com"]
  const origin = req.headers.origin
  if (allowed.includes(origin)) {
    res.setHeader("Access-Control-Allow-Origin", origin)
    res.setHeader("Vary", "Origin")  // キャッシュ汚染防止
  }
  next()
})

Vary: Origin の追加を忘れると、CDN キャッシュが「最初の Origin で固定」されて他オリジンから 403 を食らう事故が起きます。

罠2: プリフライトの 401/403

OPTIONS リクエストに対して認証ミドルウェアが動いて 401 を返してしまうケース。OPTIONS は認証チェック前に通すように設定します:

// 例: 認証ミドルウェアを OPTIONS から除外
app.use((req, res, next) => {
  if (req.method === "OPTIONS") return next()
  return authMiddleware(req, res, next)
})

罠3: サーバ側にだけ書けば直ると思ってる

CORS は「サーバが許可ヘッダを付ける、ブラウザがそれを見て判定する」という分業。サーバが Allow-Origin を返すだけでなく、クライアント側のリクエストも適切に書く必要があります(credentials, mode: "cors" など)。

罠4: Postman/curl では動くのにブラウザだけダメ

Postman / curl は SOP/CORS を実装していません。CORS はあくまでブラウザ内の制限なので、サーバから見れば普通の HTTP リクエストとして処理されます。「ブラウザだけが厳格」のは仕様です。

「CORS を回避したい」という発想は危険

ググると Access-Control-Allow-Origin: * + Access-Control-Allow-Credentials を強引に有効化したり、ブラウザの CORS 機能を無効にする拡張機能の使用を勧める記事が出てきます。絶対にやめてください

CORS は本番ユーザーの安全のための仕組み。開発時の不便を理由にユーザーを危険にさらすのは本末転倒です。代替案:

  • 開発時のプロキシ: Webpack/Vite の dev server プロキシ機能で、フロント側からは同オリジンに見せる
  • BFF(Backend for Frontend): API を直接叩かず、自分のバックエンド経由で叩く
  • 許可オリジンの正しい設定: 自社ドメインだけ許可する

セキュリティ視点での CORS

CORS の設定ミスは情報漏洩に直結する深刻な脆弱性です。OWASP も A05 Security Misconfiguration として警告しています:

  • Origin リフレクション: Origin ヘッダの値を検証なしでそのまま Allow-Origin に返す → どこからでも credentials 付きで叩ける
  • サブドメインワイルドカード: *.example.com を許可していると、攻撃者が evil.example.com を取得できれば突破
  • 古い IE / Flash 互換のための設定残骸: crossdomain.xml が残っていて全公開状態

おわりに

CORS は「動かない」と憎まれがちな仕様ですが、SOP という Web の根幹を支える防壁の上で安全に通信する仕組みです。「とりあえず *」で済ませず、許可するオリジンを明示的に管理することで、CORS は強い味方になります。

本サイトの HTTP Status Code Reference で OPTIONS / 204 / 403 など CORS 関連のステータスコードを、HTTP Cookie Parser で credentials 周りの Cookie 属性を確認できます。CORS は CSRF と表裏一体なので、合わせて読むと理解が深まります。

この記事を書いた人
akiサイバーセキュリティ実務者 / エンジニア

サイバーセキュリティ業界で実務に携わるエンジニア。脆弱性・認証・暗号・ネットワークなど、現場で必要になる知識を開発者・運用担当者向けにかみ砕いて解説しています。

運営者・編集方針について

本記事は一般的な情報提供を目的とした解説であり、特定環境での動作・安全性・成果を保証するものではありません。実際の対策の適用は、各自の環境と責任において判断してください。

Related reading

関連記事

Security15
CompTIA Security+ 完全ガイド - 難易度・出題範囲・日本語受験SY0-701 の試験形式と5つの出題ドメイン、2024年から始まった日本語受験、支援士・CISSPとの難易度比較、3年ごとの更新の仕組み、平日30分から回せる学習ロードマップを整理します。各ドメインで問われる内容は、本サイトのどの解説記事で補えるかも対応づけました。
Security16
個人情報が漏えいしたときの報告義務 - 速報・確報と本人通知の実務個人データの漏えいで個人情報保護委員会への報告が必要になるのはどんな場合か、速報・確報の期限、確報で書く9項目、本人通知の要否、委託先の免除規定を整理します。よくある「72時間以内」という誤解にも触れます。
Security18
情報セキュリティ10大脅威 2026 - IPAの順位を自社の優先順位に翻訳するIPAの「情報セキュリティ10大脅威 2026」を、順位の丸写しではなく実務の優先順位に落とし込むために読み解きます。前年からの変動、初選出された3位「AIの利用をめぐるサイバーリスク」の中身、各脅威に対応する国内事例、着手順の決め方まで整理します。
Security23
日本国内の主要セキュリティインシデント事例(2020–2025)国内の主要なセキュリティインシデント12件を一覧で俯瞰し、VPN未更新・委託先/グループ経由・弱い認証という繰り返し現れる侵入経路と失敗パターン、日本企業が今すぐ確認すべき対策チェックリストを事例研究として整理します。