CORS(Cross-Origin Resource Sharing)
ブラウザのSame-Origin Policy(同一オリジン制約)を、サーバ側の許可で選択的に緩める仕組み。 フロントとバックのドメインが違う時に必ず出てくる。
同一オリジンとは
scheme + host + port がすべて一致すること:
https://example.com:443とhttps://example.com— 同一https://example.comとhttp://example.com— 別(scheme 違う)https://www.example.comとhttps://api.example.com— 別(host 違う)https://example.comとhttps://example.com:8080— 別(port 違う)
CORS が必要な場面
- ブラウザの
fetch/ XHR で別オリジンの API を叩く - WebFont を別オリジンから読む
- Canvas に別オリジンの画像を描いて読み出す
- SW で別オリジンの fetch をキャッシュする場合(部分的)
CORS が要らない場面
- サーバ→サーバの fetch(Node.js / Cloudflare Workers)— ブラウザではないので無関係
<img>や<script>の読み込み(ただし内容アクセスには CORS)- 同一オリジン内の通信
シンプルリクエスト
以下を満たすとpreflight 不要でリクエスト送信:
- メソッドが
GET/HEAD/POST - カスタムヘッダーが少ない(
Accept/Accept-Language/Content-Language/Content-Typeのみ) Content-Typeがtext/plain/application/x-www-form-urlencoded/multipart/form-dataのみ
サーバが返すレスポンスにAccess-Control-Allow-Origin があれば、ブラウザが許可。
preflight(事前確認)
上記を満たさない場合(Content-Type: application/json など)は、ブラウザがOPTIONS リクエストを先に送る。
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
→ 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600
preflight が成功してから本リクエストが飛ぶ。Max-Age でキャッシュ可能。
サーバ側(典型的な許可レスポンス)
Access-Control-Allow-Origin: https://example.com— 許可するオリジンAccess-Control-Allow-Origin: *— 全オリジン許可。credentials: includeとは併用不可Access-Control-Allow-Methods: GET, POST, PUT— 許可メソッドAccess-Control-Allow-Headers: Content-Type, Authorization— 許可カスタムヘッダーAccess-Control-Allow-Credentials: true— Cookie / 認証情報を許可Access-Control-Expose-Headers: X-Total-Count— JS から読めるレスポンスヘッダーAccess-Control-Max-Age: 600— preflight キャッシュ秒数Vary: Origin— 動的にオリジン別の応答を返す時
クライアント側
Cookie(や Authorization ヘッダー)を別オリジンに送るには credentials 必須:
await fetch("https://api.example.com/me", {
credentials: "include", // 別オリジンに Cookie 送る
})
この時、サーバ側は:
Access-Control-Allow-Originに具体的なオリジン(*不可)Access-Control-Allow-Credentials: true
サーバ側の実装例
Express
import cors from "cors"
app.use(cors({
origin: "https://example.com",
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE"],
}))
// 動的オリジン
app.use(cors({
origin: (origin, cb) => {
const allowed = ["https://example.com", "https://staging.example.com"]
if (!origin || allowed.includes(origin)) cb(null, true)
else cb(new Error("Not allowed"))
},
credentials: true,
}))
Hono
import { cors } from "hono/cors"
app.use("/api/*", cors({
origin: ["https://example.com"],
credentials: true,
}))
素の Node.js / Cloudflare Workers
function withCors(req, response) {
const origin = req.headers.get("origin")
const allowed = ["https://example.com"]
if (origin && allowed.includes(origin)) {
response.headers.set("Access-Control-Allow-Origin", origin)
response.headers.set("Vary", "Origin")
response.headers.set("Access-Control-Allow-Credentials", "true")
}
return response
}
// preflight を最初に処理
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": req.headers.get("origin") ?? "",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Max-Age": "600",
},
})
}
典型的なエラーメッセージ
"No 'Access-Control-Allow-Origin' header"
サーバが CORS ヘッダーを返していない。サーバ側で許可設定。
"Origin is not allowed"
許可リストに無いオリジン。サーバ側のホワイトリストに追加。
"Credentials flag is true, but Access-Control-Allow-Origin is '*'"
credentials: include + Allow-Origin: * はNG。具体的オリジンを指定。
"Method not allowed by Access-Control-Allow-Methods"
preflight でそのメソッドが許可されていない。
"Header not allowed by Access-Control-Allow-Headers"
カスタムヘッダーが許可されていない。
opaque レスポンス
mode: "no-cors" や <img> の crossorigin 無しの場合、
レスポンスの中身を JS から読めない(type="opaque")。
SW でキャッシュは可能だが内容にアクセス不可。
preflight を回避する
パフォーマンスのために避けたい場合:
Content-Typeをtext/plain等に(JSON は不可)- カスタムヘッダーを使わない
- 同一オリジン経由(リバースプロキシ)に通す
同一オリジンに見せる(プロキシ)
フロントexample.com から API api.example.com を叩くより、
example.com/api/* にリバースプロキシで転送すればCORS 不要。
- Vite の
server.proxy - Next.js の
rewrites - Cloudflare Pages の
_redirects+ Worker - Nginx / Caddy の reverse proxy
// vite.config.js
export default {
server: {
proxy: {
"/api": {
target: "http://localhost:8787",
changeOrigin: true,
},
},
},
}
セキュリティの観点
- CORS は CSRF の代わりにならない — トークンや SameSite Cookie で別途対策
- Allow-Origin: * は公開 API限定(認証ありなら危険)
- 動的オリジンはホワイトリスト必須。
req.headers.originをそのまま返さない - Vary: Origin をキャッシュ対策に
- SSRF(Server-Side Request Forgery)は別問題。CORS で防げない
WebSocket と CORS
WebSocket にはCORS の概念がない。代わりに Origin ヘッダーを
サーバ側で見て許可/拒否する。
✓ ブラウザの DevTools → Network → 該当リクエスト
✓ OPTIONS リクエストが先に飛んでいるか確認
✓ レスポンスヘッダーに Access-Control-Allow-* が揃っているか
✓ credentials: include + 具体的オリジンが噛み合っているか
✓ サーバ側のログで実際に届いているか