結論(動的CORSでVary: Originを返す)
動的CORSでVary: Originを返すとは、リクエストのOriginによってAccess-Control-Allow-Originの値を変えるAPIで、応答にVary: Originを付けることです。Varyは「この要求ヘッダーの値が違えば別の応答として扱う」とキャッシュに伝えるヘッダーで、付けないとブラウザやCDNがあるオリジン向けの応答を別のオリジンにも使い回し、正規の連携先でCORSエラーが起きたり、許可していないオリジンへ許可ヘッダー付きの応答が渡ったりします。
この記事のポイント
- Vary: Origin が必要なのは、Origin によって応答(ACAO)が変わる場合だけです。
- ACAO が * や固定のオリジンなら、Fetch Standard は「常にACAOを送り、Varyは使わない」よう示しています。
- 許可しない Origin に ACAO を出さない実装でも、応答は Origin に依存しているので Vary: Origin が必要です。
- セキュリティAIチェッカーは、ACAOが * 以外の具体的な値なのに Vary に Origin も * も無い場合を△(要確認)とします。
- Apacheでは既存の Vary: Accept-Encoding を消さないよう、Header always merge を使います。
| 診断項目名 | 動的CORSでVary: Originを返す |
|---|---|
| 項目ID | headers.cors-vary |
| 分類 | CORS(クロスオリジン設定) |
| 重大度・配点の重み | 中(medium) 重み 2 |
| 関連する標準・ガイドライン | WHATWG Fetch Standard「CORS protocol and HTTP caches」/RFC 9110 §12.5.5(Vary)、RFC 9111(HTTP Caching)/OWASP ASVS 5.0.0 V3.4.2(CORS の許可リスト検証)、V14.2.5(キャッシュ機構の設定)/OWASP WSTG-CLNT-07/CWE-524(Use of Cache Containing Sensitive Information) |
動的CORSでVary: Originを返すとは
動的CORSでVary: Originを返すとは、Access-Control-Allow-Origin(以下 ACAO)の値を要求の Origin ヘッダーに応じて変える「動的 CORS」を使う場合に、応答へ Vary: Origin を付けることです。
Access-Control-Allow-Origin: https://www.city.example.lg.jp
Vary: Origin
Vary: Origin を付けないと、ブラウザや CDN・プロキシのキャッシュが「あるオリジン向けに作った応答」を、別のオリジンからの要求にも使い回します。その結果、正規の連携先で CORS エラーが起きたり、許可していないオリジンへ許可ヘッダー付きの応答が渡ったりします。
セキュリティAIチェッカーのこの項目は、API 候補の応答で ACAO が * 以外の具体的なオリジンを返しているのに、Vary に Origin が含まれていないケースを検出します。
Vary ヘッダーの役割
HTTP キャッシュは通常、「メソッド+URL」をキーにして応答を保存します。Vary は「このリクエストヘッダーの値が違えば別の応答として扱うこと」をキャッシュに伝えるヘッダーで、RFC 9110 で定義されています。
Vary: Originを付ければ、Origin ごとに別のキャッシュエントリーが作られます。Vary: *は「要求ヘッダー以外の要素でも変わる」ことを示し、キャッシュはその応答を事実上再利用できなくなります。
Vary: Originが必要な場面・不要な場面(Fetch Standardの規定)
Fetch Standard の規定
WHATWG Fetch Standard は、「CORS の要件が ACAO を * や固定のオリジンにするより複雑なら Vary を使うこと」と定めています。さらに、CORS 要求に対してだけ ACAO を返すサーバーで Vary を使わないと、先に行われた非 CORS 要求(ページ遷移など)の ACAO 無しの応答がキャッシュされ、後の CORS 要求で再利用されて失敗する、という例を挙げています。
一方で、ACAO を * や固定のオリジンにする場合は、「CORS 要求・非 CORS 要求を問わず常に ACAO を送り、Vary は使わない」よう示しています。つまり、Vary: Origin が必要なのは、Origin によって応答が変わる場合だけです。
| ACAO の返し方 | Vary: Origin |
|---|---|
| ACAO を返さない(CORS を使わない) | 不要 |
常に * を返す | 不要 |
| 常に同じ1つのオリジンを返す | 不要 |
| 許可リストに一致したときだけ、そのオリジンを返す | 必要(許可・不許可の両方の応答に付ける) |
動的 CORS で起きる問題
| 状況 | Vary: Origin なし | Vary: Origin あり |
|---|---|---|
| A 市ポータルからの要求で ACAO=A 市 が CDN に保存 → B 県サイトから要求 | ACAO=A 市 の応答が B 県に返り、B 県側で CORS エラー | Origin ごとに別キャッシュになり正常 |
| 許可オリジンの応答(ACAO 付き)が保存 → 許可していないオリジンから要求 | 許可ヘッダー付き応答が再利用される可能性 | 別エントリーとして再生成 |
| 非 CORS 要求(ACAO なし)が保存 → 許可オリジンから CORS 要求 | ACAO なし応答が再利用され CORS エラー | 正常 |
許可しない Origin に対して ACAO を出さない実装でも、応答は Origin に依存しているため Vary: Origin を付けるべきです。
動的CORSのVary: Originについてよくある誤解
- 「Vary はブラウザには関係ない」:ブラウザの HTTP キャッシュも Vary を考慮します。
- 「全 API に Vary: Origin を付ければよい」:固定 ACAO や CORS を使わない API に付けても害は小さいですが、CDN のキャッシュ効率は下がります。Fetch Standard は、固定値の場合は Vary を使わないよう示しています。
- 「Vary: Origin でセキュリティ問題が解決する」:Vary はキャッシュの混同を防ぐだけです。Origin の反射そのものは「任意Originを反射しない」の問題として、別途修正が必要です。
動的CORSでVary: Originを返すの診断基準(セキュリティAIチェッカー)
取得対象
「任意Originを反射しない」と同じ応答を使います。トップページから抽出した同一オリジンの API 候補(/api/ 配下、最大3件)へ Origin: https://ver2-invalid-origin.example を付けた GET を送り、この項目のための追加のリクエストは行いません。
判定条件
| 判定 | 条件 | 得点への影響 |
|---|---|---|
| ○ 問題なし(pass) | ACAO が具体的な値の応答すべてで、Vary に Origin または * を含む | 重み2 × 1.00 |
| △ 要確認(warn) | ACAO が * 以外の具体的な値なのに、Vary をカンマで分割した値に origin も * も無い(大文字・小文字は区別しない) | 重み2 × 0.45 |
| 対象外(na) | API 候補が無い、応答を取得できない、または ACAO が無いか * だけ(Origin に依存しない設定) | 計算対象外 |
- 診断は1種類の Origin でしか要求しないため、ACAO が検証用オリジンと異なる固定値(例:常に自サイトのオリジン)の場合、本当に固定なのか、許可リスト外のときだけ既定値を返す動的実装なのかを外部から区別できません。そのため × ではなく △(要確認)としています。固定値であれば Fetch Standard 上は Vary 不要なので、設定を確認のうえ説明できれば問題ありません。
Vary: *の応答はVary: Originと同じく ○ 側で扱います。キャッシュはその応答を再利用できず、Origin による混同が起きないためです。ただしキャッシュ効率が下がるため、Vary: Originへの変更を検討してください。- ACAO が検証用オリジンと一致した(反射した)場合は、確実に動的な設定です。その場合は
headers.cors-reflectionが × になり、この項目も Vary の有無で判定されます。 - 証拠欄には「n/m 件で Vary: Origin がありません」のように件数を表示します。
- 許可リストに無い Origin には ACAO を返さない実装では、検証用オリジンへの応答に ACAO が無いため対象外になります。この場合 Vary: Origin が欠けていても外部からは検出できないので、許可済みの Origin を付けた要求で手動確認してください(後述の「設定後の確認方法」)。
自治体サイトでの判定例
| サイトの状態 | 判定 |
|---|---|
オープンデータ API が Access-Control-Allow-Origin: * | 対象外(Vary 不要) |
県・市町村共同の観光情報 API が許可リスト方式で ACAO を切り替え、許可リスト外の Origin には既定値として県ポータルのオリジンを返すが、Vary は Accept-Encoding のみ | △ |
同じ API が Vary: Origin, Accept-Encoding | ○ |
| 同じ API が許可リスト外の Origin には ACAO を返さない | 対象外(Vary の有無は手動確認) |
Vary: Originが無いとどんなリスクがあるか
- 可用性の問題:CDN が最初に来たオリジン向けの応答を保存し、他の連携先自治体のサイトで地図やイベント情報が表示されなくなります。障害の原因が分かりにくく、時間帯によって再現したりしなかったりします。
- 許可範囲の混乱:許可済みオリジン向けの ACAO 付き応答が共有キャッシュから他の要求者に返り、意図しない読み取りを許す可能性があります。ただし、ACAO の値が要求元と一致しなければブラウザは応答を共有しないため、悪用の条件は限定的です。
- キャッシュポイズニングとの組み合わせ:Origin を反射する設定と共有キャッシュが組み合わさると、攻撃者の Origin を含む応答がキャッシュされ、他の利用者にも配信される場合があります。
- この項目単独で重大な情報漏えいに直結した実例は確認できていないため、重大度は medium としています。
障害シナリオの例(県・市町村共同の観光情報API)
- 県の観光ポータルと A 市のサイトが、同じ観光情報 API を JavaScript で読み込んでいる。API は許可リスト方式で ACAO を切り替えるが、
Vary: Originを返していない。 - 朝、A 市サイトからの要求で
ACAO: https://www.city-a.example.lg.jp付きの応答が CDN に保存される。 - 続いて県ポータルから要求すると、CDN は保存済みの A 市向けの応答を返す。ACAO が要求元と一致しないため、ブラウザは応答を渡さず、県ポータルでは観光情報が表示されない。
- キャッシュが切れると、今度は県ポータル向けの応答が保存され、A 市側で同じ障害が起きる。
Vary: Origin があれば、CDN は Origin ごとに別のエントリーを保存するため、この障害は起きません。
動的CORSでVary: Originを返す設定方法(Apache・nginx・IIS・PHP・Next.js)
基本方針
- CORS の要否を決める。外部から読ませないなら ACAO 自体を出さない(Vary も不要)。
- 公開データは
Access-Control-Allow-Origin: *を常に返す(Vary 不要)。 - 許可リストで切り替える場合は、許可・不許可にかかわらず、その API の全応答に
Vary: Originを付ける。
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
# 既存の Vary(Accept-Encoding 等)を消さないよう merge を使う
Header always merge Vary "Origin"
</IfModule>
Header set Vary Origin にすると、mod_deflate が付けた Accept-Encoding を上書きすることがあるため、merge を使います。
nginx
# $cors_origin は map で許可リストと完全一致したときだけ Origin を入れる変数
#(「任意Originを反射しない」の nginx 例を参照)
location /api/ {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Vary Origin always;
}
gzip を使う場合、nginx は gzip_vary on; で Vary: Accept-Encoding を別に付けます。両方が出力されていれば問題ありません。
IIS
IIS CORS Module の公式リファレンスは、* だけの規則では ACAO が * 以外にならないため Vary: origin を付与しないと説明しています。特定オリジンの規則を使う場合は、実際の応答に Vary: Origin が出ていることを確認してください。自作の URL Rewrite で ACAO を設定している場合は、customHeaders 等で Vary を追加します。
PHP
<?php
header('Vary: Origin', false); // 第2引数 false で既存の Vary を置き換えない
$allowed = ['https://www.city.example.lg.jp'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowed, true)) {
header('Access-Control-Allow-Origin: ' . $origin);
}
Next.js / Vercel
export async function GET(req: Request) {
const origin = req.headers.get("origin") ?? "";
const headers = new Headers({ Vary: "Origin" });
if (ALLOWED.has(origin)) headers.set("Access-Control-Allow-Origin", origin);
return Response.json(data, { headers });
}
Middleware で CORS ヘッダーを付ける場合も、全応答に Vary を付けてください。CDN キャッシュ(s-maxage)を使う API では特に重要です。
CDN
CloudFront などでは、キャッシュキーに Origin ヘッダーを含める設定(オリジンリクエストポリシー/キャッシュポリシー)も必要になる場合があります。利用している CDN の仕様書で Vary の扱いを確認してください。
設定後の確認方法
for o in https://www.city.example.lg.jp https://attacker.example; do
echo "== $o"
curl -s -o /dev/null -D - -H "Origin: $o" https://www.city.example.lg.jp/api/xxx \
| grep -iE '^(access-control-allow-origin|vary):'
done
許可オリジンでは ACAO が返り、不許可オリジンでは ACAO が無く、両方で Vary: Origin が出ていれば正しい構成です。
サイトを壊さないための注意点(ガードレール)
- Vary に多くのヘッダーを並べると、CDN のヒット率が下がります。Origin 以外を安易に追加しないでください。
- 設定変更後は CDN のキャッシュをパージし、古い応答が残らないようにします。
- 固定の ACAO(
*や1つのオリジン)に統一できるなら、それが最も単純で安全です。
よくある質問(FAQ)
Vary: Origin はどんなときに必要ですか?
Origin ヘッダーの値に応じて Access-Control-Allow-Origin を切り替える「動的CORS」のときに必要です。許可リスト方式で連携先ごとに ACAO を返す API が典型です。
Access-Control-Allow-Origin: * のときも Vary: Origin は必要ですか?
不要です。Fetch Standard は、ACAO を * や固定のオリジンにする場合は、CORS要求・非CORS要求を問わず常に ACAO を送り、Vary は使わないよう示しています。診断でもこの場合は「対象外」になります。
Vary はブラウザには関係ないのではありませんか?
関係します。CDNやプロキシだけでなく、ブラウザの HTTP キャッシュも Vary を考慮します。
Vary: Origin を付ければCORSのセキュリティ問題は解決しますか?
解決しません。Vary はキャッシュの混同を防ぐだけです。Origin をそのまま反射する設定は「任意Originを反射しない」(headers.cors-reflection)の問題として別に修正が必要です。
Vary: * でもよいですか?
診断上は Vary: Origin と同じく問題なし側で扱います。キャッシュはその応答を再利用できず、Origin による混同が起きないためです。ただしキャッシュ効率が下がるので、Vary: Origin への変更を検討してください。
ACAOが常に自サイトのオリジンなのに△になりました。なぜですか?
診断は1種類の Origin でしか要求しないため、本当に固定値なのか、許可リスト外のときだけ既定値を返す動的実装なのかを外部から区別できないためです。固定値であれば Vary は不要なので、設定を確認して説明できれば問題ありません。
この項目を含む127項目を、URLを入れるだけで無料診断します。