「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/foo | https://example.com:443 | 基準 |
| https://example.com/bar | https://example.com:443 | 同じ ✅ |
| http://example.com/foo | http://example.com:80 | 違う ❌(スキーム違い) |
| https://api.example.com | https://api.example.com:443 | 違う ❌(ホスト違い) |
| https://example.com:8080 | https://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 送信fetchやXMLHttpRequestでのリクエスト送信自体(レスポンスが読めないだけ)
最後の項目が大事。リクエストは飛んでサーバ側で副作用が起きる。これが 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-Typeがapplication/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: 86400OPTIONS で OK が返ったら、ブラウザは本リクエスト(PUT)を送信。OPTIONS が失敗したら本リクエストは送られないので、サーバ側に副作用は起きません。これがプリフライトの安全性。
典型的にプリフライトが発生するケース:
fetch(url, { method: "PUT" })やDELETEAuthorization: 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: true と Access-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 付きで叩ける状態は危険すぎる」というブラウザベンダーの判断。
よくある罠
罠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 と表裏一体なので、合わせて読むと理解が深まります。