CVE-2026-46342_vulnerability_report_20260522
CVE-2026-46342 Vulnerability Report (詳細版)
概要
| 項目 | 内容 |
|---|---|
| CVE番号 | CVE-2026-46342 |
| 公開日 | 2026-05-19 |
| 最終更新 | 2026-05-22 |
| CVSS v3.1 | 7.5 (High) — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N |
| CVSS v4.0 | 2.3 (Low) — CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N |
| CVSS v2.0 | 6.4 (Medium) — CVSS2#AV:N/AC:L/Au:N/C:P/I:P/A:N |
| CWE | CWE-79 (XSS), CWE-349 (Acceptance of Extraneous Untrusted Data), CWE-444 (HTTP Response Smuggling) |
| GHSA ID | GHSA-g8wj-3cr3-6w7v |
| 影響製品 | Nuxt (npm) — experimental.componentIslands 有効時 |
| EPSS | 調査時点 (2026-05-22) で公開スコア未確認 |
| CISA KEV | なし (2026-05-22時点) |
| 修正担当者 | @danielroe (Nuxtコアメンテナ) |
影響を受けるソフトウェアおよびバージョン
- 製品名: Nuxt (npmパッケージ)
- 前提条件:
experimental.componentIslandsが有効 (Nuxt 4ではデフォルト'auto'、Nuxt 3ではオプトイン) - 影響範囲: Nuxt 3.x / 4.x (Component Islands機能を使用している全バージョン)
- 修正バージョン:
nuxt@3.21.6/nuxt@4.4.6 - 修正PR: nuxt/nuxt#35077
- 修正コミット:
ef8db85(fix/island-props → main) - CPE: 【情報不足】(NVDに未登録のためCPE定義なし)
技術詳細
脆弱性の概要
Nuxtの /__nuxt_island/* エンドポイントにおけるキャッシュポイズニングおよび保存型XSS (Stored XSS) 脆弱性。
/__nuxt_island/* エンドポイントは、クエリ/ボディパラメータ経由で攻撃者制御の props を受け取り、アイランドコンポーネントをレンダリングする。しかし、URL内に埋め込まれたハッシュ (<Name>_<hashId>.json) が、実際にそのpropsに対して <NuxtIsland> によって発行されたものかどうかをサーバーサイドで検証していない。
ハッシュはクライアントサイドで計算されURLに埋め込まれるが、サーバーサイドでは検証されないため、同じパスがクエリに応じて materially different なレスポンスを返すことができる。
根本原因 — コードレベル分析
修正前に追加された共有ユーティリティ: packages/nuxt/src/app/island-hash.ts (NEW)
// ✅ 修正PRで新規作成された共有ハッシュ計算ロジック
/** Vueスコープスタイル属性 (data-v-*) をアイランドpropsから除去してからハッシュ計算 */
export function filterIslandProps (props: Record<string, any> | null | undefined): Record<string, any> {
if (!props) { return {} }
const out: Record<string, any> = {}
for (const key in props) {
if (!key.startsWith('data-v-')) {
out[key] = props[key]
}
}
return out
}
/** アイランドURLに埋め込まれる hashId セグメントを計算 */
export function computeIslandHash (
name: string,
filteredProps: Record<string, any>,
context: Record<string, any>,
source: string | undefined,
): string {
return hash([name, filteredProps, context, source]).replace(/[-_]/g, '')
}
設計意図: クライアント/サーバーで同一のハッシュ計算ロジックを使用し、URL内のハッシュが実際のpropsと一致していることを検証可能にする。
修正前: packages/nuxt/src/app/components/nuxt-island.ts (インライン実装)
// ❌ 脆弱な実装 — クライアントサイドのみでハッシュ計算、サーバーサイドでは検証なし
const filteredProps = computed(() =>
props.props ? Object.fromEntries(
Object.entries(props.props).filter(([key]) => !key.startsWith('data-v-'))
) : {}
)
const hashId = computed(() =>
hash([props.name, filteredProps.value, props.context, props.source]).replace(/[-_]/g, '')
)
問題点: ハッシュはクライアントサイドで計算されURLに埋め込まれるが、サーバーサイドではこのハッシュを検証しない。任意のpropsを渡すことで任意のレンダリング結果を得られる。
修正後: packages/nitro-server/src/runtime/handlers/island.ts (サーバーサイド検証)
// ✅ 修正後の実装 — サーバーサイドでハッシュを検証
const rawContext = event.req.method === 'GET'
? getQuery<NuxtIslandContext>(event)
: await readBody<NuxtIslandContext>(event)
const rawProps = destr<Record<string, any> | null | undefined>(rawContext?.props) || {}
const filteredProps = filterIslandProps(rawProps)
// クライアントコンテキストの再構築
// (クライアントは { ...props.context, props: JSON.stringify(props.props) } を送信)
const clientContext: Record<string, any> = {}
if (rawContext && typeof rawContext === 'object') {
for (const key in rawContext) {
if (key !== 'props') clientContext[key] = rawContext[key]
}
}
// 🔒 レスポンスをURLにバインド: ハッシュが実際のペイロードと一致しない場合は拒否
const expectedHash = computeIslandHash(componentName, filteredProps, clientContext, undefined)
if (!hashId || hashId !== expectedHash) {
throw new HTTPError({ status: 400, statusText: 'Invalid island request hash' })
}
注意点: CodeRabbitレビューで指摘された通り、computeIslandHash の第4引数 (source) が undefined でハードコードされている。本来は filteredProps?.source を渡すべきだが、これはクライアント/サーバー間のハッシュ計算ミスマッチを招く可能性があり、PRマージ後の追加修正が必要となる可能性がある。
クライアントコンポーネントの修正: packages/nuxt/src/app/components/nuxt-island.ts
- const filteredProps = computed(() =>
- props.props ? Object.fromEntries(
- Object.entries(props.props).filter(([key]) => !key.startsWith('data-v-'))
- ) : {}
- )
- const hashId = computed(() =>
- hash([props.name, filteredProps.value, props.context, props.source]).replace(/[-_]/g, '')
- )
+ const filteredProps = computed(() => filterIslandProps(props.props))
+ const hashId = computed(() =>
+ computeIslandHash(props.name, filteredProps.value, props.context, props.source)
+ )
修正コミット詳細 (ef8db85)
| 項目 | 内容 |
|---|---|
| 変更ファイル数 | 7ファイル |
| 新規ファイル | packages/nuxt/src/app/island-hash.ts (共有ユーティリティ), packages/nuxt/test/island-hash.test.ts (ユニットテスト) |
| 変更ファイル | packages/nitro-server/src/runtime/handlers/island.ts, packages/nuxt/src/app/components/nuxt-island.ts, test/server-components.test.ts |
| 追加依存 | packages/nitro-server/package.json に `ohash: |
| .0.11` 追加 | |
| パフォーマンス影響 | loadNuxt ベンチマークで -13.63% の回帰 (環境差分の可能性あり、要監視) |
ハッシュ検証の動作
| シナリオ | サーバーレスポンス |
|---|---|
| ✅ 有効なハッシュ (props/contextと一致) | 200 OK |
| ❌ 異なるpropsで計算されたハッシュ | 400 Bad Request |
| ❌ 偽造/無効なハッシュ | 400 Bad Request |
| ❌ URLにハッシュセグメントなし | 400 Bad Request |
| 📦 デフォルトキャッシュヘッダー | cache-control: private | no-store<br>vary: cookie |
ユニットテストカバレッジ (island-hash.test.ts)
filterIslandProps:null/undefinedの処理、通常propsの透過、data-v-*プレフィックスの除去、data-v-を含むがプレフィックスでないキーの保持computeIslandHash:ohashシェイプとの一致、props/context/name/source変更時の予測可能な変化、URLセーフな出力 (-と_なし)
E2Eセキュリティテスト (server-components.test.ts)
- 正しいハッシュの受け入れ
- タンペリングされたクエリの拒否
- 偽造ハッシュの拒否
- 保守的なキャッシュヘッダーの強制
攻撃ベクトルと影響
-
キャッシュポイズニング:
- CDN/リバースプロキシが
/__nuxt_island/*をパスのみでキャッシュ (クエリ文字列を無視) している場合 - 攻撃者が悪意ある
propsでキャッシュをプリム(事前に埋め込み) - 後続の正当なユーザーが同じパスにリクエストすると、攻撃者が仕込んだレンダリング済みHTMLを受け取る
- キャッシュエントリは通常の期限切れまで持続
- CDN/リバースプロキシが
-
保存型XSSへのエスカレーション:
- ポイズニングされたpropsがアプリケーションコード内の安全でないHTMLシンク (
v-html,innerHTML, サードパーティレンダラー) に流れる場合 - 埋め込みページのオリジンで保存型XSSが発生
HttpOnlyCookieは保護されるが、それ以外のオリジンデータ (通常のCookie、DOM状態、オリジン内リクエスト) はインジェクションされたスクリプトから到達可能
- ポイズニングされたpropsがアプリケーションコード内の安全でないHTMLシンク (
必要条件
| 条件 | 説明 |
|---|---|
experimental.componentIslands が有効 |
'auto' または明示的に有効化されていること |
| 共有中間キャッシュの存在 | CDN/リバースプロキシがパスのみでキャッシュキーを設定 (クエリを無視) |
| XSS成立のため追加条件 | アプリケーション作成のアイランドが、propsを安全でないHTMLシンクに渡していること |
キャッシュポイズニングのみ: 第2条件が必要。キャッシュがクエリ文字列も含む場合、影響なし。
XSSのみ: 第3条件が必要。アイランドがpropsを安全にレンダリングする場合、影響はcontent-swap/不活性HTMLインジェクションに低減。
重要なアーキテクチャ上の注意事項
"It's important to remember that route middleware does not run when rendering island components, and islands cannot rely on routing-layer auth."
- ページミドルウェアの背後で機密データをゲートしているアプリケーションは、アイランド自身のデータレイヤー内で認証を強制する必要がある (サーバー専用ルート、
useRequestEvent+ 手動セッションチェック等) - これはドキュメント化された動作でありバグではない。パッチ適用後も引き続き該当
- 別途、
*.server.vueページがpage_<routeName>アイランドとして登録される場合、ドキュメント化された「ミドルウェアなし」契約がdefinePageMeta({ middleware })宣言と衝突し、 genuine bug となる別件のアドバイザリが存在
修正内容
nuxt@4.4.6 および nuxt@3.21.6 (PR #35077) で修正。
修正メカニズム:
- サーバーが
<NuxtIsland>がクライアントサイドで使用するのと同じohash関数を使用して、(name, props, context)から期待されるhashIdを再計算 - URL内のハッシュが一致しないリクエストは
HTTP 400で拒否 - レスポンスはリクエストパスの純粋な関数となり、パスキーの共有キャッシュがすべてのリクエスターに正しいレスポンスを返す
- 攻撃者が任意のpropsに対して有効なハッシュを持つパスを合成できなくなる
URLバインディング形式
修正前:
/__nuxt_island/<Name>.json?props={...}
修正後:
/__nuxt_island/<Name>_<hashId>.json?props={...}
hashId は (name, filteredProps, context, source) の ohash によるSHA-256ベースハッシュ (Base64URLエンコード、- と _ を除去)。
ワークアラウンド
即時アップグレードが不可能な場合:
- キャッシュ設定の検証: 中間キャッシュが
/__nuxt_island/*をフルURL (クエリ文字列含む) でキー設定していることを確認。Netlifyはデフォルトでクエリ文字列ごとにキャッシュするため安全。Cloudflare等のCDNで "Cache Everything" + クエリ無視設定をしている場合は危険 - アイランドの監査: アプリケーション作成のアイランドで、propsが
v-html/innerHTML/ 類似のHTMLシンクに流れていないか確認。アイランドpropsは信頼できないユーザー入力として扱う - 認証境界の確認: ページミドルウェアのみに依存する認証をアイランド内で使用していないか確認。アイランドサーバーハンドラー内で明示的にセッション/認証チェックを実装
- 未使用の場合無効化: アイランド機能を使用していない場合、
experimental.componentIslandsをfalseに設定して攻撃面を排除
関連脆弱性
- CVE-2026-45669 (GHSA-fx6j-w5w5-h468): 同一セキュリティアップデートで公開された
navigateTo()反射型XSS - CVE-2026-47200: ルートミドルウェアバイパス —
.server.vueページがアイランドエンドポイント経由でミドルウェアを迂回 - GHSA-jvhm-gjrh-3h93 (CVE-2025-27415): CDN/リバースプロキシのパスのみキャッシュ設定に関するドキュメント化された誤設定クラス
- CVE-2026-45670: 開発サーバーのソースコード露出
影響度
成功したエクスプロイトにより以下のリスクが発生:
- 保存型XSS: キャッシュポイズニング経由で永続的なスクリプトインジェクション
- コンテンツの改ざん: キャッシュされたレスポンスの書き換えによるページ内容の改ざん
- セッションハイジャッキング: HttpOnly以外のCookie、DOM状態へのアクセス
- データ窃取: オリジン内のリクエストやDOM経由の機密情報取得
- なりすまし: 攻撃者が仕込んだHTML/スクリプトが正当なコンテンツとして配信
検知方法
- 依存関係スキャンで
nuxtパッケージのバージョンを確認 experimental.componentIslandsが有効か確認 (nuxt.config.ts/nuxt.config.js)- CDN/リバースプロキシの設定でキャッシュキーがクエリ文字列を含むか確認
- アイランドコンポーネント内で
v-html/innerHTMLの使用箇所を監査 /__nuxt_island/*エンドポイントのログで不審なpropsパラメータを監視
参考情報
| ソース | URL |
|---|---|
| GitHub Advisory | https://github.com/advisories/GHSA-g8wj-3cr3-6w7v |
| GitLab Advisory | https://advisories.gitlab.com/npm/@nuxt/nitro-server/CVE-2026-46342 |
| Feedly CVE | https://feedly.com/cve/CVE-2026-46342 |
| Tenable | https://www.tenable.com/cve/CVE-2026-46342 |
| Resolved Security | https://www.resolvedsecurity.com/vulnerability-catalog/CVE-2026-46342 |
| Netlify Changelog | https://www.netlify.com/changelog/2026-05-19-nuxt-security-vulnerabilities |
| 修正PR #35077 | https://github.com/nuxt/nuxt/pull/35077 |
| 修正コミット ef8db85 | https://github.com/nuxt/nuxt/commit/ef8db85 |
推奨対応
- 直ちに
nuxt@3.21.6またはnuxt@4.4.6以降へアップグレード - CDN/リバースプロキシのキャッシュ設定を再検証 — クエリ文字列をキャッシュキーに含める
- アイランドコンポーネント内の
v-html/innerHTML使用箇所を監査・修正 - アイランドの認証境界を確認 — ページミドルウェアのみに依存していないか確認
- アイランド未使用時は
experimental.componentIslands: falseで無効化
実サイト影響確認手順 (Impact Verification Guide)
⚠️ 重要: 以下の手順は非破壊的な検証方法のみを使用します。本番環境で悪意のあるペイロードを送信するテストは絶対に行わないこと。キャッシュポイズニングが発生すると、全ユーザーに改ざんコンテンツが配信されるリスクがあります。
影響判定フロー
CVE-2026-46342が実際に悪用可能な状態にあるかどうかは、5つの条件がすべて満たされているかどうかで決まります。いずれか1つでも満たされなければ、実質的な影響はありません。
[Step 1] componentIslands が有効か?
↓ はい
[Step 2] /__nuxt_island/ エンドポイントが外部から到達可能か?
↓ はい
[Step 3] CDN/リバースプロキシが「パスのみ」をキャッシュキーにしているか?
↓ はい
[Step 4] アイランドコンポーネントがユーザー入力をunsafe HTML sinkに渡しているか?
↓ はい
[Step 5] 攻撃者propsでキャッシュを汚染可能か?(非破壊的テスト)
Step 1: Component Islands の有効化確認
判定基準: experimental.componentIslands が true または { localOnly: true } になっているか。デフォルト値はバージョンによって異なる。
確認方法A: nuxt.config.ts の直接確認(ホワイトボックス)
# プロジェクトルートで実行
grep -rn "componentIslands" nuxt.config.ts nuxt.config.js nuxt.config.mjs
結果判定:
componentIslands: false→ 影響なし(機能が無効)componentIslands: trueまたは{ localOnly: true }→ Step 2へ- 設定が存在しない → バージョンを確認(v3.19〜v3.20以降はデフォルトで有効化されている場合がある)
確認方法B: 実行時チェック(グレーボックス)
# ビルド成果物の .output/server/chunks/nitro/ 配下で確認
grep -r "componentIslands" .output/
確認方法C: HTTPプローブ(ブラックボックス)
curl -s -o /dev/null -w "%{http_code}" https://<target-site>/__nuxt_island/TestIsland
結果判定:
404→ エンドポイントが存在しない → 影響なし200または500→ エンドポイントが存在 → Step 2へ
Step 2: /__nuxt_island/ エンドポイントの外部到達性確認
⚠️ 重要: 存在しないアイランド名(
NonExistentIsland_abc123.jsonなど)を送信すると、アイランドが有効でも404が返ります。このテストは常に実際に存在するアイランド名を使用する必要があります。
判定基準: 実際のアイランド名を特定し、/__nuxt_island/<IslandName>_<ohashId>.json へのHTTPリクエストが外部から200系のレスポンスを返すか。
テストコマンド:
# まず実在するアイランド名を特定する(以下のいずれか)
# 方法1: ページHTMLソースから <NuxtIsland> コンポーネントのname属性を抽出
curl -s https://<target-site>/ | grep -oP 'data-island-uid[^>]*|NuxtIsland[^>]*name="([^"]+)"'
# 方法2: クライアントJSバンドルから __nuxt_island/ への参照を抽出
curl -s https://<target-site>/ | grep -oP 'src="[^"]*\.js"' | while read js; do
curl -s "https://<target-site>/${js#\"}" | grep -oP '__nuxt_island/[A-Za-z0-9_]+' | head -5
done
# 方法3: ブラウザDevToolsのNetworkタブで /__nuxt_island/ へのリクエストを観察
# → 実際のアイランド名とハッシュが確認できる
実在するアイランド名(例: MyHeader_abc123)が特定できたら:
curl -v https://<target-site>/__nuxt_island/MyHeader_abc123.json
結果判定:
404 Not Found→ アイランドがプリレンダリングされていない、またはエンドポイントが無効 → 低リスク200 OKまたは500 Internal Server Error→ エンドポイントがアクティブ → Step 3へ
Step 3: CDN/リバースプロキシのキャッシュ設定確認
最も重要な判定ステップ。キャッシュキーの設定が攻撃の成否を分けます。
判定基準: CDNが /__nuxt_island/* へのリクエストをキャッシュする際、クエリ文字列をキャッシュキーに含めているか。
テストコマンド:
# Test 3a: クエリなしのレスポンス
curl -s -D- https://<target-site>/__nuxt_island/<IslandName>_<ohashId>.json \
-H "Cache-Control: max-age=0" 2>&1 | grep -iE "cache|x-cache|x-vercel|cf-cache"
# Test 3b: クエリ付きのレスポンス(異なるprops)
curl -s -D- "https://<target-site>/__nuxt_island/<IslandName>_<ohashId>.json?props=eyJ0ZXN0IjoxfQ==" \
-H "Cache-Control: max-age=0" 2>&1 | grep -iE "cache|x-cache|x-vercel|cf-cache"
キャッシュヘッダーの解釈:
| ヘッダー | 値 | 意味 |
|---|---|---|
x-cache: MISS |
(初回) → HIT (2回目) |
キャッシュ有効 |
cf-cache-status: HIT |
Cloudflare | キャッシュ有効 |
x-vercel-cache: HIT |
Vercel | キャッシュ有効 |
cache-control: public, max-age=N |
N > 0 | キャッシュ有効 |
cache-control: no-store |
— | キャッシュ無効 → 影響なし |
cache-control: private |
— | ブラウザのみキャッシュ、CDNはしない → 低リスク |
クエリ文字列がキャッシュキーに含まれているか確認:
# 2つの異なるpropsでリクエスト → 異なるキャッシュエントリなら安全
curl -s "https://<target-site>/__nuxt_island/<IslandName>_<ohashId>.json?props=A" | head -c 200
curl -s "https://<target-site>/__nuxt_island/<IslandName>_<ohashId>.json?props=B" | head -c 200
結果判定:
- 両者が異なるレスポンスを返す → クエリがキャッシュキーに含まれている → 低リスク(キャッシュポイズニング不可)
- 両者が同じレスポンスを返す(かつHIT)→ パスのみキャッシュキー → 高リスク → Step 4へ
Step 4: アイランドコンポーネントのunsafe HTML sink確認
判定基準: アイランドコンポーネントがユーザー入力(props)を v-html や innerHTML でレンダリングしているか。
確認方法A: ソースコード検索(ホワイトボックス・推奨)
# アイランドディレクトリ内を検索
grep -rn "v-html\|innerHTML\|dangerouslySetInnerHTML" components/islands/
# または、すべてのアイランドコンポーネント(.server.vue)を検索
grep -rn "v-html\|innerHTML" components/**/*.server.vue
結果判定:
- 該当なし → 中リスク(コンテンツの書き換えは可能だが、XSSは不可)
- 該当あり → Step 5へ(高リスクの可能性)
確認方法B: 脆弱なパターンの特定
以下のようなコードパターンが存在する場合、XSSに直結する可能性があります:
<!-- 🚨 危険: propsを直接v-htmlでレンダリング -->
<template>
<div v-html="props.userContent" />
</template>
<!-- 🚨 危険: propsを文字列結合してinnerHTML -->
<script setup>
const props = defineProps(['html'])
document.getElementById('output').innerHTML = props.html
</script>
確認方法C: 実行時チェック(グレーボックス・注意が必要)
# 既存のprops構造を確認(非破壊)
curl -s "https://<target-site>/__nuxt_island/<IslandName>_<hash>.json" | python3 -m json.tool | head -50
Step 5: キャッシュ汚染の再現テスト(非破壊的・Staging推奨)
⚠️ 警告: このテストはStaging環境でのみ実行してください。本番環境で実行すると、実際のキャッシュ汚染が発生する可能性があります。
テストコマンド(Staging環境):
# 1. benignなpropsでキャッシュをプリロード
curl -s "https://<staging-site>/__nuxt_island/<IslandName>_<ohashId>.json?props=eyJtZXNzYWdlIjoiYmVmb3JlIn0=" \
-H "Cache-Control: max-age=0"
# 2. 異なるpropsでリクエスト → 元のキャッシュが返ってくるか確認
curl -s "https://<staging-site>/__nuxt_island/<IslandName>_<ohashId>.json?props=eyJtZXNzYWdlIjoiYWZ0ZXIifQ==" \
-H "Cache-Control: max-age=0"
結果判定:
- Step 1のレスポンスが返る → キャッシュポイズニング成功 → 影響あり
- Step 2のpropsに応じたレスポンスが返る → 影響なし(修正済みまたはキャッシュ設定が適切)
判定マトリックス(最終結論)
| Step 1 | Step 2 | Step 3 | Step 4 | 結論 | 推奨アクション |
|---|---|---|---|---|---|
| ❌ 無効 | — | — | — | 影響なし | 機能未使用。設定変更不要 |
| ✅ 有効 | ❌ 404 | — | — | 影響なし | エンドポイント非公開。念のためWAFでブロック |
| ✅ 有効 | ✅ 200 | ❌ クエリ付きキャッシュ | — | 低リスク | CDN設定が適切。v-html監査を推奨 |
| ✅ 有効 | ✅ 200 | ✅ パスのみキャッシュ | ❌ unsafe sinkなし | 中リスク | content-swap可能。XSSは不可。v-html監査 + CDN設定見直し |
| ✅ 有効 | ✅ 200 | ✅ パスのみキャッシュ | ✅ unsafe sinkあり | 高リスク | 即時アップグレード + CDN設定変更 + v-html修正 |
CDN別キャッシュ設定チェックリスト
| CDN/プロキシ | 確認箇所 | 安全な設定 |
|---|---|---|
| Cloudflare | Cache Rules / Page Rules | Cache Everything で Cache Deception ARMOR を有効化 |
| Vercel | vercel.json の headers |
Cache-Control: public, s-maxage=N にクエリパラメータを含める |
| CloudFront | Cache Behavior | Cache key and cache policies で QueryString を含める |
| Nginx | proxy_cache_key |
$scheme$proxy_host$request_uri(クエリ含む) |
| Fastly | VCL vcl_hash |
req.url 全体(クエリ含む)をハッシュキーに |