0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

#0203(2025/08/10)CORSを正しく使う

Posted at

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-Typeapplication/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;
}

よくある誤解とアンチパターン

  1. サーバ間通信にもCORSが必要:いいえ。CORSはブラウザが強制する仕組み。curl やサーバ間では無関係。
  2. ``を付けておけば楽:認証付き要求ができず、かえって運用が複雑化。特定オリジンを明示する。
  3. プリフライトが遅い=悪:要件次第。Max-Age と設計で最小化し、早期最適化しすぎない
  4. “CORSでCSRFは防げる”:レイヤが違う。CORSは読み取り制御、CSRFは状態変更対策で別途実装。

トラブルシューティング・チェックリスト

  • ブラウザDevToolsのNetworkRequest Headers/Response Headers/Console を確認

  • Origin 値は期待通りか?(null になるケースに注意:file://, sandbox iframe等)

  • サーバが返す Access-Control-Allow-*プリフライト要求と整合しているか?

  • 資格情報を使うとき:

    • フロント credentials: "include" / withCredentials: true
    • サーバ Allow-Credentials: trueAllow-Origin はワイルドカード不可
    • Cookie の SameSite/Secure/HttpOnly 設計は妥当か
  • リバースプロキシ(CDN/ELB/Nginx)でヘッダーが落ちていないか


まとめ(TL;DR)

  • CORSはSOPを安全に緩める標準。鍵は OriginAccess-Control-Allow-*
  • プリフライトは「事前の許可取り」。“単純でない”操作は OPTIONS→OK→本リクエストの2段階。
  • Cookie×CORScredentials: include(クライアント)と Allow-Credentials: true+特定 Allow-Origin(サーバ)のセット運用
  • 認証は不可。ホワイトリストや動的反映で正確にオリジンを縛る
  • CSRFは別対策(SameSite, CSRFトークン, Origin/Referer検証)。
  • 迷ったらDevToolsでヘッダー整合性を見て、Max-Age やヘッダー最小化で体感を改善する。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?