認証情報を許可するCORSでの動的なOrigin

認証情報を許可するCORSでの動的なOrigin

説明

リクエストの Origin や他の入力を検証せず Access-Control-Allow-Origin に反映し、Access-Control-Allow-Credentials: true と組み合わせると、信頼できないオリジンに応答の読み取りを許可するおそれがあります。認証情報の送信は、クライアントのリクエストオプションやCookieのSameSite・サードパーティーポリシーにも依存します。その条件で認証済みの応答を悪意のあるオリジンから読めると、機密情報が漏えいする可能性があります。

CORSはブラウザーによる応答の読み取りを制御します。サーバーの認証・認可やCSRF対策の代わりにはなりません。CORSの応答ヘッダーがなくても、サーバーへ到達するクロスオリジンリクエストがあります。

想定される影響

  • 認証情報付きリクエストの応答を悪意のあるオリジンから読み取られ、機密情報が漏えいするおそれがあります。
  • サーバーの認証・認可やCSRF対策も不十分な場合、ユーザーの権限でデータを変更される可能性があります。
  • IDORなど他のアクセス制御の問題と組み合わさり、追加のリソースを取得・変更されるおそれがあります。
  • ブラウザーから到達可能な内部APIの応答が、誤ったオリジンに公開される可能性があります。

対処方法

  • 許可する正確なオリジンを Set や Map に登録し、一致する値だけを Access-Control-Allow-Origin に設定してください。正規表現を使う場合も、スキーム・ホスト・ポートを厳密に固定してください。
  • 検証していないOrigin、クエリ、ヘッダーをそのまま反映しないでください。機密の応答に Access-Control-Allow-Origin: "null" を使わないでください。
  • 許可したオリジンにだけ Access-Control-Allow-Credentials: true を設定し、それ以外では省略するか403で拒否してください。
  • オリジンによって応答が変わる場合は Vary: Origin を返し、キャッシュが区別できるようにしてください。
  • OPTIONSのプリフライトを検証し、必要なメソッド・ヘッダーだけを許可して、それ以外の組み合わせを拒否してください。
  • SameSiteでクロスサイトのCookie送信を制限し、Secureで転送経路、HttpOnlyでスクリプトからのアクセスを保護してください。機密性の高い操作には、別途CSRF検証を適用してください。

例

CORSヘッダーを比較する抜粋です。認証・認可とCSRFの処理は省略しています。

変更前

javascript
const express = require("express");
const app = express();

// Originやクエリをそのまま反映し、常に認証情報を許可
app.use((req, res, next) => {
  const o = req.get("Origin") || req.query.o || "null";
  res.set("Access-Control-Allow-Origin", o);
  res.set("Access-Control-Allow-Credentials", "true");
  next();
});

app.get("/profile", (req, res) => {
  res.json({ email: "user@example.com" });
});

変更後

javascript
const express = require("express");
const app = express();

// 正確な許可リストとVary: Originを適用
const ALLOWLIST = new Set([
  "https://app.example.com",
  "https://admin.example.com",
]);

app.use((req, res, next) => {
  const origin = req.get("Origin");
  if (origin && ALLOWLIST.has(origin)) {
    res.set("Access-Control-Allow-Origin", origin);
    res.set("Access-Control-Allow-Credentials", "true");
    res.set("Vary", "Origin");
  } else {
    // 一致しないオリジンにはCORSの許可を返さない(または403)
    // res.status(403).end(); // ポリシーに応じて選択
  }
  next();
});

// プリフライトの例
app.options("/profile", (req, res) => {
  const origin = req.get("Origin");
  if (origin && ALLOWLIST.has(origin)) {
    res.set("Access-Control-Allow-Origin", origin);
    res.set("Access-Control-Allow-Credentials", "true");
    res.set("Access-Control-Allow-Methods", "GET");
    res.set("Access-Control-Allow-Headers", "Content-Type");
    res.set("Vary", "Origin");
    return res.sendStatus(204);
  }
  return res.sendStatus(403);
});

app.get("/profile", (req, res) => {
  res.json({ email: "user@example.com" });
});

解説:

  • 変更前: 検証していないオリジンを反映し、認証情報付きの応答の読み取りを許可します。リクエストオプションとCookieポリシーが送信を許可すると、悪意のあるオリジンが認証済みの応答を読めるおそれがあります。
  • 変更後: 許可リストと完全一致するオリジンにだけ応答の読み取りを許可し、プリフライトにも同じリストを適用します。ヘッダーを省略しても、すべてのリクエストやCookie送信を防げるわけではありません。Vary: Origin はオリジンに応じて応答が変わることをキャッシュへ伝えます。

参考資料