CORSを実務で正しく使い倒す
CORSとは「同一オリジン制約を安全に緩和するために、ブラウザがサーバの許可を確認してから他オリジンへアクセスする仕組みである。」
前提:なぜCORSが必要か(SOPの背景)
ブラウザには同一オリジンポリシー(SOP)があり、スキーム・ホスト・ポートが一致しないリソースへの読み書きを既定で禁止します。これにより、悪意のあるサイトがあなたのセッションで他サイトの機密情報を盗むことを防いでいます。ただし、マイクロサービスやフロント/バック分離など正当なクロスオリジン通信も必要です。ここで使うのが**CORS(Cross-Origin Resource Sharing)**です。
CORSの基本概念
-
主体:ブラウザ(強制力のある実装)/サーバ(許可を宣言する)
-
オリジン:
scheme://host:port
の三要素で一意。どれか1つでも違えば別オリジン。 -
やっていること:
- ブラウザがOriginヘッダーを付けてリクエスト
- サーバがAccess-Control-Allow-* ヘッダーで許可を明示
- 条件により、事前確認(プリフライト) を実施
全体像(通信の流れ)
[ブラウザ] --(1) OPTIONS + Origin, A-C-Request-* --> [サーバ]
[ブラウザ] <--(2) 200 + A-C-Allow-* -------------- [サーバ]
[ブラウザ] --(3) 本リクエスト(GET/POST...) ------> [サーバ]
[ブラウザ] <--(4) レスポンス + A-C-Allow-Origin -- [サーバ]
- (1)(2) は プリフライト。必要なときだけ発生。
- (3)(4) が実データのやりとり。
プリフライトリクエストとは?
役割:実データ送信前に、サーバがそのクロスオリジン操作を許可するか事前確認する。
発生条件(“単純”でない場合):
- メソッドが
GET/HEAD/POST
以外(PUT/DELETE/PATCH 等) - 送信ヘッダーにカスタムや認証系(例:
Authorization
,X-*
) -
Content-Type
がapplication/json
など非単純(単純はtext/plain
,multipart/form-data
,application/x-www-form-urlencoded
)
例(ブラウザ→サーバ):
OPTIONS /api/items HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
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
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600
実務TIPS(回避・最適化):
-
application/json
を送るだけでプリフライトになる → 可能ならmultipart/form-data
/x-www-form-urlencoded
で代替 - カスタムヘッダーを減らす/標準化
-
Access-Control-Max-Age
を設定してブラウザの許可結果キャッシュを延長
比較表:単純リクエスト vs プリフライトが必要なリクエスト
観点 | 単純リクエスト | プリフライト必要 |
---|---|---|
メソッド | GET/HEAD/POST | PUT/DELETE/PATCH など |
Content-Type |
text/plain / multipart/form-data / application/x-www-form-urlencoded
|
application/json など |
リクエストヘッダー | 簡易な標準ヘッダーのみ |
Authorization , X-* など含む |
ブラウザ動作 | 直接送る | 先に OPTIONS で事前確認 |
レイテンシ | 低い | 高い(OPTIONS 往復の分だけ増加) |
レスポンスヘッダー整理(覚えるコアセット)
ヘッダー | 役割 | 典型値/例 | 重要注意 |
---|---|---|---|
Access-Control-Allow-Origin |
どのオリジンを許可するか |
https://app.example.com / *
|
* は 資格情報(クッキー等)と併用不可
|
Access-Control-Allow-Methods |
許可メソッド | GET, POST, PUT |
プリフライト応答で提示 |
Access-Control-Allow-Headers |
許可ヘッダー | Content-Type, Authorization |
要求されたヘッダーを網羅 |
Access-Control-Expose-Headers |
フロントJSから読めるレスポンスヘッダー | X-Total-Count |
デフォだと多くのヘッダーは読めない |
Access-Control-Allow-Credentials |
資格情報(Cookie, Authorization 等)同送を許可 | true |
これを付けるなら Allow-Origin に * は不可 |
Access-Control-Max-Age |
プリフライト結果のキャッシュ秒数 | 600 |
ブラウザ依存あり(上限有) |
CookieとCORSの関係(認証・状態管理)
何ができる?:Cookie(セッションID等)を付けて認証済みの要求を送れる。
ブラウザ側設定(例):
// fetch
fetch("https://api.example.com/secure", {
credentials: "include", // Cookie等の資格情報を送る
});
// axios
axios.get("https://api.example.com/secure", { withCredentials: true })
サーバ側要件:
Access-Control-Allow-Credentials: true
-
Access-Control-Allow-Origin: https://app.example.com
(*
は不可) - Cookie自体の属性:
Secure
(HTTPS必須),HttpOnly
(JSからアクセス不可),SameSite
(後述)
SameSiteとCORSの位置関係(概念図):
Cookie配布/送信の可否 … SameSite が決める
クロスオリジン読取可否 … CORS が決める(ブラウザ実装)
SameSite属性:
値 | 挙動 | 典型用途 |
---|---|---|
Strict |
他サイト遷移/埋め込みではCookie送信しない | 高セキュリティ、UX犠牲 |
Lax |
ほとんど送らないが、トップレベルGET遷移は送る | 既定に近い安全ライン |
None + Secure
|
クロスサイトでも送る | SPA+別ドメインAPI 等で必要 |
Access-Control-Allow-Credentials: true
の役割と注意
役割:ブラウザに「このクロスオリジン通信で資格情報を使ってよい」と伝える。
仕様上の制約:
- 併用NG:
Access-Control-Allow-Origin: *
とAllow-Credentials: true
- プリフライト応答にも
Allow-Credentials: true
を返すこと - 返すオリジンは動的に一致させる(ホワイトリストで検証後に
Origin
をそのまま反映 等)
CSRF対策は別レイヤ:
- 認証付CORSを許す=CSRFの成立条件が整いやすい
- 対策セット:
SameSite=Lax/None+Secure
の設計、CSRFトークン、Referer/Origin検証、Idempotent設計
実装スニペット(最小構成)
Node.js(Express):
import cors from 'cors';
const app = express();
const whitelist = ["https://app.example.com"]; // 実運用は環境変数等で
app.use(cors({
origin: (origin, cb) => cb(null, whitelist.includes(origin) ? origin : false),
credentials: true,
methods: ["GET","POST","PUT","DELETE"],
allowedHeaders: ["Content-Type","Authorization"],
maxAge: 600,
}));
FastAPI:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
origins = ["https://app.example.com"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"]
)
Nginx(リバースプロキシ):
location /api/ {
if ($http_origin = "https://app.example.com") {
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Max-Age 600 always;
}
if ($request_method = OPTIONS) { return 204; }
proxy_pass http://backend;
}
よくある誤解とアンチパターン
-
サーバ間通信にもCORSが必要:いいえ。CORSはブラウザが強制する仕組み。
curl
やサーバ間では無関係。 - ``を付けておけば楽:認証付き要求ができず、かえって運用が複雑化。特定オリジンを明示する。
-
プリフライトが遅い=悪:要件次第。
Max-Age
と設計で最小化し、早期最適化しすぎない。 - “CORSでCSRFは防げる”:レイヤが違う。CORSは読み取り制御、CSRFは状態変更対策で別途実装。
トラブルシューティング・チェックリスト
-
ブラウザDevToolsのNetworkで
Request Headers
/Response Headers
/Console
を確認 -
Origin 値は期待通りか?(
null
になるケースに注意:file://, sandbox iframe等) -
サーバが返す
Access-Control-Allow-*
はプリフライト要求と整合しているか? -
資格情報を使うとき:
- フロント
credentials: "include" / withCredentials: true
- サーバ
Allow-Credentials: true
、Allow-Origin
はワイルドカード不可 - Cookie の
SameSite/Secure/HttpOnly
設計は妥当か
- フロント
-
リバースプロキシ(CDN/ELB/Nginx)でヘッダーが落ちていないか
まとめ(TL;DR)
-
CORSはSOPを安全に緩める標準。鍵は
Origin
とAccess-Control-Allow-*
。 - プリフライトは「事前の許可取り」。“単純でない”操作は OPTIONS→OK→本リクエストの2段階。
-
Cookie×CORS は
credentials: include
(クライアント)とAllow-Credentials: true
+特定Allow-Origin
(サーバ)のセット運用。 - 認証は不可。ホワイトリストや動的反映で正確にオリジンを縛る。
- CSRFは別対策(SameSite, CSRFトークン, Origin/Referer検証)。
- 迷ったらDevToolsでヘッダー整合性を見て、
Max-Age
やヘッダー最小化で体感を改善する。