別ドメインに置いた API をフロントエンドから fetch したら、ブラウザのコンソールが赤くなる。「Access to fetch … has been blocked by CORS policy」。受託開発でフロントとバックを別々に立てると、ほぼ必ず一度はこの壁にぶつかります。困ったときに検索すると、上位に出てくるのは「Access-Control-Allow-Origin: * を付ければ直る」という回答。貼ったら確かにエラーは消える。しかしそれは、鍵のかかったドアを「全員入れる」に設定して開かないようにしただけかもしれません。
Hacker News で「開発者は CORS を理解していない」という記事が定期的に話題になるのは、この「エラーは消えたが意味は分かっていない」状態が、いかに広く残っているかの証拠です。受託で納品する API なら、なおさら「とりあえず全許可」のまま出すわけにはいきません。本記事では、CORS が何を守る仕組みなのかを腹落ちさせたうえで、受託でやりがちな危険な設定を、安全な形に直す道筋を整理します。
CORS は「サーバーを守る」のではなく「ユーザーを守る」仕組み
最初の誤解の元は、「CORS はサーバーへのアクセスを制限するセキュリティ機能だ」という思い込みです。実際は逆に近い。CORS はブラウザが、利用者を守るためにかけている制限です。
前提にあるのは「同一オリジンポリシー」というブラウザの大原則です。あるサイト(オリジン)で動く JavaScript が、別のオリジンのリソースを勝手に読み取れないようにする——これがなければ、悪意あるサイトを開いただけで、別タブで開いているネットバンキングの情報を盗み読むコードが動いてしまいます。CORS は、この厳しい原則に対して「このオリジンになら、読み取りを許してよい」とサーバー側が明示的に例外を出すための仕組みです。
つまり Access-Control-Allow-Origin ヘッダーは、サーバーがブラウザに「このオリジンからのアクセスは、レスポンスを読ませてよい」と伝える許可証です。エラーが出ているのは「サーバーがそのオリジンを許可していない」から。これを *(全オリジン許可)で消すのは、許可証を「誰でも可」にすることに等しい。だからエラーは消えますが、守りたかったものまで開けてしまう。
ここを取り違えると、API 連携の設計が根本からぶれます。考え方の軸は、認証情報の置き場所をどう扱うかとも密接に関わります。Cookie とトークンの扱いについては、JWT・Cookie・セッションの設計を監査する記事で扱った観点と合わせて見ると、CORS と認証の関係が立体的に理解できます。
なぜ「とりあえず全許可」が危ないのか
Access-Control-Allow-Origin: * を貼る対処が危険になるのは、特に認証情報を伴う API のときです。
ブラウザの仕様上、*(全許可)と「Cookie 等の認証情報を送る設定(credentials)」は同時に使えません。そのため、認証付き API で全許可にしようとすると、今度は「リクエスト元のオリジンをそのまま許可ヘッダーに反射させる」という、さらに危険な実装に走りがちです。
// やってはいけない例:来たオリジンを無検証でそのまま許可する
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", req.headers.origin); // 危険
res.header("Access-Control-Allow-Credentials", "true");
next();
});
これは「どのオリジンから来ても、そのオリジンを許可する」=実質的に全許可で、かつ認証情報まで送れる状態です。悪意あるサイトからのリクエストにも認証付きで応答してしまい、CORS が本来守っていたものが完全に無効になります。「エラーが消えた」の裏で、ユーザーを守る仕組みを自分で壊しているわけです。
受託で採るべき正しい設定
正しいのは、* でも「来たものを反射」でもなく、許可するオリジンを明示的に列挙する(許可リスト方式)です。
| よくある対処 | 何が起きるか | 受託での評価 |
|---|---|---|
Allow-Origin: * | 全オリジンに読み取り許可。認証情報は送れない | 公開・無認証 API 以外は不可 |
| 来たオリジンを無検証で反射 | 実質全許可+認証情報も送れる | 危険。納品物に入れてはいけない |
| 許可リストで照合して返す | 登録したオリジンだけ許可 | これが基本 |
実装は、リクエストのオリジンが「あらかじめ決めた許可リストに含まれるか」を確認し、含まれるときだけそのオリジンを許可ヘッダーに返す形にします。
const allowedOrigins = ["https://app.example.com", "https://admin.example.com"];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header("Access-Control-Allow-Origin", origin);
res.header("Access-Control-Allow-Credentials", "true");
}
next();
});
許可リストは環境変数などで本番・ステージング・開発を切り替えられるようにし、コードに直書きしないのが運用上のコツです。また、PUT や DELETE、独自ヘッダーを使う場合はブラウザが事前に OPTIONS リクエスト(プリフライト)を送るため、Access-Control-Allow-Methods と Access-Control-Allow-Headers も合わせて返す必要があります。API ゲートウェイ層での設定ミスがセキュリティの穴になる点は、API Gatewayの設定不備が認可バイパスを生む記事で扱ったケースとも通じます。
受託で直したときの実例
弊社がセキュリティ観点でレビューを依頼されたある SaaS の API(社名は伏せます)では、フロントとバックを別ドメインで運用しており、開発中に出た CORS エラーを「来たオリジンをそのまま反射する」実装で塞いでいました。認証は Cookie ベースで、Allow-Credentials: true も付いていたため、どのサイトからでも認証付きでこの API を叩ける状態になっていました。本人たちは「CORS は設定済み」と認識しており、まさか全開放になっているとは思っていませんでした。
私たちはこれを許可リスト方式に置き換え、本番・ステージング・ローカルのオリジンを環境変数で管理する形にしました。やったのは「来たものを無条件で許す」のを「登録したものだけ許す」に変えただけです。あわせてプリフライトの応答も整理し、不要なメソッド・ヘッダーの許可を削りました。機能上の見た目は何も変わりませんが、悪意あるサイトからの認証付きアクセスは弾かれるようになりました。
この案件で一番効いたのは、「CORS エラーが消えた=正しく設定できた、ではない」とチームで共有したことでした。エラーは消し方をいくらでも間違えられます。何を守る仕組みかを理解してはじめて、消し方の良し悪しが判断できる。理解が先、設定は後です。
どこから着手するか
まず、自分たちの API が CORS をどう設定しているかを実際に確認するのが出発点です。Access-Control-Allow-Origin に * が入っている、あるいは来たオリジンをそのまま返している箇所があれば、それは見直し対象です。認証情報を扱う API なら、許可するオリジンを明示的に列挙する方式に変え、許可リストを環境ごとに管理する形へ整えれば、機能を変えずに守りを取り戻せます。
別ドメイン構成で CORS の設定に自信が持てない、「全許可」のまま本番に出てしまっている、API のセキュリティを一度棚卸ししたい——そうしたお悩みがあれば、グリームハブのお問い合わせからご相談ください。現行 API の CORS と認証まわりを拝見し、機能を止めずに安全な設定へ整える形でお手伝いします。