• 診断項目 No.99/127
  • CORS(クロスオリジン設定)
  • 重大度 重要(high)

任意Originを反射しないとは?CORSのOrigin反射の危険性・許可リストの設定方法・診断基準

任意Originを反射しないとは、CORSでリクエストのOriginをそのままAccess-Control-Allow-Originに返さないことです。Allow-Credentialsとの併用で情報が盗まれる仕組み、許可リストの正しい書き方、診断基準を解説します。

結論(任意Originを反射しない)

任意Originを反射しないとは、CORSの応答でリクエストのOriginヘッダーの値をそのままAccess-Control-Allow-Originに書き戻さず、固定値か、信頼済みの許可リストと完全一致したオリジンだけを返すことです。反射は「どのサイトからでも読んでよい」と宣言するのと同じで、Access-Control-Allow-Credentials: trueと組み合わさると、攻撃者のサイトを開いただけでログイン中の利用者のAPI応答を読み取られます。

この記事のポイント

  • Origin を反射すると、ACAOが常に要求元と一致する値になり、ワイルドカード(*)に対するブラウザ側の安全装置をすり抜けます。
  • Allow-Credentials: true との併用では、罠ページを開いただけで氏名・申請履歴・CSRFトークンなどを読み取られます。
  • 許可リストは完全一致で照合します。前方一致・後方一致、ドットをエスケープしない正規表現、null の許可は典型的な誤りです。
  • セキュリティAIチェッカーは /api/ の候補に実在しない検証用Originを付けてGETし、反射または「* と Credentials の併用」があれば×と判定します。
  • CORSは同一オリジンポリシーを緩める仕組みで、認可やCSRF対策の代わりにはなりません。
診断項目名任意Originを反射しない
項目IDheaders.cors-reflection
分類CORS(クロスオリジン設定)
重大度・配点の重み重要(high) 重み 4
関連する標準・ガイドラインOWASP ASVS 5.0.0 V3.4.2(Access-Control-Allow-Origin は固定値か、Origin を信頼済み許可リストで検証すること)/OWASP WSTG-CLNT-07(Cross Origin Resource Sharing)/CWE-942(Permissive Cross-domain Security Policy with Untrusted Domains)、CWE-346(Origin Validation Error)/WHATWG Fetch Standard(CORS protocol)

任意Originを反射しないとは(CORSのOrigin反射)

任意Originを反射しないとは、CORS(Cross-Origin Resource Sharing)の応答で、リクエストの Origin ヘッダーの値をそのまま Access-Control-Allow-Origin(以下 ACAO)に書き戻さないことです。ACAO には固定値を返すか、信頼済みの許可リストと完全一致したオリジンだけを返します。

CORS は、ブラウザが「別のサイトの JavaScript に、このサイトの応答を読ませてよいか」を判断する仕組みです。サーバーは ACAO で、読み取りを許すオリジンを宣言します。

実装を簡単にするために、要求の Origin の値をそのまま ACAO に書き戻す(反射する)サーバーがあります。これは「どのサイトからでも読んでよい」と宣言しているのと同じです。さらに Access-Control-Allow-Credentials: true を併用していると、攻撃者のサイトから、利用者のログイン状態のまま API の応答を読み取れてしまいます。

Origin: https://evil.example
Access-Control-Allow-Origin: https://evil.example
Access-Control-Allow-Credentials: true

セキュリティAIチェッカーのこの項目は、API の候補に存在しないオリジンを付けて要求し、その値が反射されないかを確認します。

CORSの仕組みとOrigin反射が危険な理由

CORS の基本動作

  1. ブラウザは、別オリジンへの fetch() などの要求に Origin: https://evil.example を付けて送ります。
  2. サーバーの応答に Access-Control-Allow-Origin: https://evil.example(または *)があれば、ブラウザはその JavaScript に応答の本文を渡します。
  3. Cookie 付き(credentials mode が include)の要求では、ACAO が * では応答が共有されません。要求元オリジンと一致する値と、Access-Control-Allow-Credentials: true の両方が必要です(WHATWG Fetch Standard の CORS protocol and credentials の表)。

反射が危険な理由

  • ACAO に * を書いても、ブラウザは Cookie 付き要求の応答を共有しません。ところが Origin を反射すると、ACAO は常に「要求元と一致する値」になるため、ワイルドカードに対するブラウザ側の安全装置をすり抜けます。
  • Allow-Credentials: true と組み合わさると、攻撃者のサイトを開いただけで、利用者の Cookie 付き要求の応答(氏名・メール・申請履歴・CSRF トークン等)を読み取られます。PortSwigger Web Security Academy は、これを代表的な CORS の脆弱性として説明しています。
  • Credentials が無くても、反射は ASVS V3.4.2 の「固定値または許可リスト検証」に反します。ASVS は * を使う場合は応答に機微情報を含まないことを求めており、反射も同じ扱いで確認が必要です。また、IP アドレスで保護された庁内向け資源のように Cookie を使わない保護では、ワイルドカード相当の許可でも外部サイト経由で読み取られる可能性があります。

許可リスト実装の典型的な誤り

Origin を検証しているつもりでも、照合の仕方が甘いと実質的に反射と同じになります。

誤り例結果
後方一致city.example.lg.jp で終わるかを判定evilcity.example.lg.jp を許可してしまう
前方一致https://city.example.lg.jp で始まるかを判定city.example.lg.jp.evil.example を許可してしまう
ドットをエスケープしない正規表現city.example の .任意の1文字に一致してしまう
null オリジンの許可Origin: null を許可リストに入れるsandbox iframe やローカルファイルから送られる null を、攻撃者も作り出せる

任意Originの反射についてよくある誤解

  • 「CORS は攻撃を防ぐ仕組み」:CORS は同一オリジンポリシーを緩める仕組みであり、設定を誤ると守りを外すことになります。要求の送信自体は止めないため、CSRF 対策にもなりません。
  • 「ACAO を付けなければ API は読まれない」:ブラウザ経由では読まれませんが、curl 等からは読めます。CORS は認可の代わりにはなりません。

任意Originを反射しないの診断基準(セキュリティAIチェッカー)

取得対象

  • トップページの HTML から抽出した、同一オリジンの API 候補を最大3件。候補は、/api/ で始まるリンク・GET フォーム、/api/ へ POST する form の action、HTML やインライン JS の中の "/api/..." という文字列です。logout/delete 等を含むパスは除外します。
  • 各候補へ Origin: https://ver2-invalid-origin.example(実在しない検証用オリジン)を付けて GET を1回送ります。状態を変える POST は送りません。
  • リダイレクトは最大3回まで追跡し、同一オリジン外へ転送された候補は除外します。

判定条件

判定条件得点への影響
× 要改善(fail)いずれかの応答の ACAO が検証用オリジンと完全一致(反射)、または ACAO が * かつ Access-Control-Allow-Credentials: true重み4 × 0
○ 問題なし(pass)取得できた全応答で上記に該当しない(ACAO 無し、固定のオリジン、Credentials 無しの *、null を含む)重み4 × 1.00
対象外(na)API 候補が見つからない、または全候補の取得に失敗した計算対象外
  • * と Credentials の併用はブラウザが拒否するため、直接の悪用にはつながりません。それでも「認証付きで外部へ公開したい」という危険な意図・設定ミスの兆候として × にしています。
  • 反射が見つかった場合、Credentials の有無で深刻度が変わります。証拠を確認し、Credentials も許可されていれば最優先で修正してください。
  • Access-Control-Allow-Origin: null と、プリフライト(OPTIONS)での反射は「CORSプリフライトの許可範囲」、トップページでの * や Credentials の組み合わせは「危険なCORSワイルドカードがない」と「CORSの認証情報設定に矛盾がない」で評価します。Origin ごとに ACAO を変える場合の Vary は「動的CORSでVary: Originを返す」で確認します。

自治体サイトでの判定例

サイトの状態判定
静的 CMS で /api/ が無い対象外
防災情報の JSON API /api/bousai/latest が Access-Control-Allow-Origin: *(Credentials なし)○(公開データであれば適切)
施設予約システムの /api/user/profile が任意の Origin を反射し、Allow-Credentials: true×(利用者情報が第三者サイトから読まれる恐れ)

Originを反射するとどんなリスクがあるか

  • ログイン中の利用者の情報窃取:攻撃者がメールや SNS で罠ページへ誘導 → 罠ページの JS が fetch("https://yoyaku.city.example.lg.jp/api/user", {credentials:"include"}) を実行 → 応答を攻撃者のサーバーへ送信、という流れです。利用者の操作は「ページを開く」だけです。
  • CSRF トークンの窃取:応答に含まれるトークンを読まれ、CSRF 対策を無効化されます。
  • 庁内資源の読み取り:職員の端末から到達できる内部 API が反射設定だと、職員が外部サイトを閲覧しただけで内部情報を読まれる可能性があります。
  • 実例:PortSwigger の James Kettle 氏は 2016 年の調査「Exploiting CORS misconfigurations for Bitcoins and bounties」で、Origin の反射や許可リスト判定の誤りにより、暗号資産取引所を含む実在のサービスで利用者データや API キーを読み取れた事例を報告しています。

攻撃シナリオの例(施設予約システムの場合)

  1. 住民Aは市の施設予約システムにログインしたまま、別のタブでメールを読む。
  2. メールのリンクから攻撃者の罠ページを開く。
  3. 罠ページの JavaScript が、住民Aのブラウザから予約システムの /api/user へ Cookie 付きで要求を送る。
  4. 予約システムは罠ページの Origin を反射し、Allow-Credentials: true も返すため、ブラウザは応答を罠ページに渡す。
  5. 住民Aの氏名・連絡先・予約履歴が攻撃者のサーバーへ送られる。

許可リストで完全一致の照合をしていれば、手順4で ACAO が返らず、ブラウザは応答を罠ページに渡しません。

なお、手順3で Cookie が送られるのは、ブラウザがクロスサイトの要求にセッション Cookie を付ける場合(Cookie が SameSite=None で、ブラウザのサードパーティ Cookie 制限の対象にならない場合など)です。SameSite=Lax・Strict は被害を減らしますが、同じサイト(登録可能ドメイン)内の別サブドメインからの要求には効かないため、それに頼らず CORS の許可リスト自体を正しく設定してください。

任意Originを反射しないCORSの設定方法(Apache・nginx・PHP・IIS・Next.js)

基本方針

  1. 外部サイトから読ませる必要がない API には ACAO を付けない(同一オリジンの画面からの利用には CORS は不要です)。
  2. 公開データ(オープンデータ・防災情報)で誰でも読んでよいものは * を固定で返し、Cookie を使わない。
  3. 特定の連携先にだけ許可する場合は、完全一致の許可リストで照合し、一致したときだけその値を返す。Credentials が必要な場合も同様です。

Apache(.htaccess)

<IfModule mod_headers.c>
  SetEnvIf Origin "^(https://(www\.city\.example\.lg\.jp|portal\.pref\.example\.lg\.jp))$" CORS_OK=$1
  Header always set Access-Control-Allow-Origin "%{CORS_OK}e" env=CORS_OK
  Header always merge Vary "Origin"
</IfModule>

正規表現は ^ と $ で全体一致にし、ドットをエスケープします。Header set Access-Control-Allow-Origin "%{HTTP_ORIGIN}e" のような無条件の反射は使わないでください。

nginx

map $http_origin $cors_origin {
    default "";
    "https://www.city.example.lg.jp"     $http_origin;
    "https://portal.pref.example.lg.jp"  $http_origin;
}
server {
    location /api/ {
        add_header Access-Control-Allow-Origin $cors_origin always;  # 空値なら出力されない
        add_header Vary Origin always;
    }
}

PHP

<?php
$allowed = ['https://www.city.example.lg.jp', 'https://portal.pref.example.lg.jp'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
header('Vary: Origin', false);
if (in_array($origin, $allowed, true)) {   // 厳密な完全一致
    header('Access-Control-Allow-Origin: ' . $origin);
    // Cookie 連携が本当に必要な場合だけ
    // header('Access-Control-Allow-Credentials: true');
}

IIS

IIS CORS Module の <cors enabled="true" failUnlistedOrigins="true"> と <add origin="https://www.city.example.lg.jp" /> で許可リストを定義します(一覧外の Origin は 403)。Microsoft の設定リファレンスは、allowCredentials は * ではなく特定オリジンの規則にだけ使うよう注意しています。

Next.js(Route Handler)

const ALLOWED = new Set(["https://www.city.example.lg.jp"]);
export async function GET(req: Request) {
  const origin = req.headers.get("origin") ?? "";
  const headers = new Headers({ "Content-Type": "application/json", Vary: "Origin" });
  if (ALLOWED.has(origin)) headers.set("Access-Control-Allow-Origin", origin);
  return new Response(JSON.stringify({ ok: true }), { headers });
}

設定後の確認方法

curl -s -o /dev/null -D - -H "Origin: https://attacker.example" \
  https://www.city.example.lg.jp/api/xxx | grep -i '^access-control-'
# access-control-allow-origin: https://attacker.example が出たら反射している
curl -s -o /dev/null -D - -H "Origin: null" https://www.city.example.lg.jp/api/xxx | grep -i '^access-control-'
curl -s -o /dev/null -D - -H "Origin: https://www.city.example.lg.jp.attacker.example" \
  https://www.city.example.lg.jp/api/xxx | grep -i '^access-control-'

3つ目のコマンドは、前方一致の誤った許可リストを見つけるための確認です。いずれでも ACAO に要求した Origin が返らなければ正しい構成です。

サイトを壊さないための注意点(ガードレール)

  • 許可リストを厳しくすると、正規の連携先(県のポータル、外部の地図サービス等)が読み取れなくなる場合があります。変更前に、アクセスログの Origin を集計して実際の連携先を洗い出してください。
  • 動的に ACAO を変える場合は Vary: Origin を付け、キャッシュの混同を防ぎます(「動的CORSでVary: Originを返す」参照)。
  • CORS は認可の代わりになりません。API 側でセッション・権限のチェックを必ず行ってください。

よくある質問(FAQ)

CORSのOrigin反射とは何ですか?

リクエストの Origin ヘッダーの値を検証せず、そのまま Access-Control-Allow-Origin に書き戻す実装です。どのオリジンからの要求にも許可を返すため、「どのサイトからでも応答を読んでよい」と宣言しているのと同じになります。

Access-Control-Allow-Origin: * と Origin反射は同じですか?

同じではありません。* の場合、ブラウザはCookie付き要求の応答を共有しません。反射ではACAOが要求元と一致する値になるため、この安全装置をすり抜け、Allow-Credentials: true と組み合わさると認証付きの応答まで読まれます。

Cookieを使わないAPIなら、Originを反射しても安全ですか?

安全とは言えません。反射は ASVS V3.4.2 の「固定値または許可リスト検証」に反し、応答に機微情報を含まないことの確認が必要です。また、IPアドレスで保護された庁内向け資源のように Cookie を使わない保護では、外部サイト経由で読み取られる可能性があります。

Origin: null を許可してもよいですか?

許可すべきではありません。null は sandbox iframe やローカルファイルから送られる値で、攻撃者も null を作り出せます。null の扱いは「CORSプリフライトの許可範囲」(cors.preflight-policy)でも評価します。

CORSを設定すればCSRF対策になりますか?

なりません。CORSは同一オリジンポリシーを緩める仕組みで、要求の送信自体は止めません。また、ACAOを付けなくても curl などからは読めるため、APIの認可の代わりにもなりません。

CORSのOrigin反射が診断で×になった場合、何から直せばよいですか?

証拠を確認し、Access-Control-Allow-Credentials も許可されていれば最優先で修正します。外部から読ませる必要がない API は ACAO を付けない、公開データは * を固定で返す、連携先がある場合は完全一致の許可リストにする、の順で方針を決めます。

セキュリティAIチェッカーを利用する

この項目を含む127項目を、URLを入れるだけで無料診断します。

参考資料