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の /__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)

E2Eセキュリティテスト (server-components.test.ts)

攻撃ベクトルと影響

  1. キャッシュポイズニング:

    • CDN/リバースプロキシが /__nuxt_island/*パスのみでキャッシュ (クエリ文字列を無視) している場合
    • 攻撃者が悪意ある props でキャッシュをプリム(事前に埋め込み)
    • 後続の正当なユーザーが同じパスにリクエストすると、攻撃者が仕込んだレンダリング済みHTMLを受け取る
    • キャッシュエントリは通常の期限切れまで持続
  2. 保存型XSSへのエスカレーション:

    • ポイズニングされたpropsがアプリケーションコード内の安全でないHTMLシンク (v-html, innerHTML, サードパーティレンダラー) に流れる場合
    • 埋め込みページのオリジンで保存型XSSが発生
    • HttpOnly Cookieは保護されるが、それ以外のオリジンデータ (通常のCookie、DOM状態、オリジン内リクエスト) はインジェクションされたスクリプトから到達可能

必要条件

条件 説明
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."

修正内容

nuxt@4.4.6 および nuxt@3.21.6 (PR #35077) で修正。

修正メカニズム:

URLバインディング形式

修正前:

/__nuxt_island/<Name>.json?props={...}

修正後:

/__nuxt_island/<Name>_<hashId>.json?props={...}

hashId(name, filteredProps, context, source)ohash によるSHA-256ベースハッシュ (Base64URLエンコード、-_ を除去)。

ワークアラウンド

即時アップグレードが不可能な場合:

  1. キャッシュ設定の検証: 中間キャッシュが /__nuxt_island/* をフルURL (クエリ文字列含む) でキー設定していることを確認。Netlifyはデフォルトでクエリ文字列ごとにキャッシュするため安全。Cloudflare等のCDNで "Cache Everything" + クエリ無視設定をしている場合は危険
  2. アイランドの監査: アプリケーション作成のアイランドで、propsが v-html / innerHTML / 類似のHTMLシンクに流れていないか確認。アイランドpropsは信頼できないユーザー入力として扱う
  3. 認証境界の確認: ページミドルウェアのみに依存する認証をアイランド内で使用していないか確認。アイランドサーバーハンドラー内で明示的にセッション/認証チェックを実装
  4. 未使用の場合無効化: アイランド機能を使用していない場合、experimental.componentIslandsfalse に設定して攻撃面を排除

関連脆弱性

影響度

成功したエクスプロイトにより以下のリスクが発生:

検知方法

  1. 依存関係スキャンで nuxt パッケージのバージョンを確認
  2. experimental.componentIslands が有効か確認 (nuxt.config.ts / nuxt.config.js)
  3. CDN/リバースプロキシの設定でキャッシュキーがクエリ文字列を含むか確認
  4. アイランドコンポーネント内で v-html / innerHTML の使用箇所を監査
  5. /__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

推奨対応

  1. 直ちに nuxt@3.21.6 または nuxt@4.4.6 以降へアップグレード
  2. CDN/リバースプロキシのキャッシュ設定を再検証 — クエリ文字列をキャッシュキーに含める
  3. アイランドコンポーネント内の v-html / innerHTML 使用箇所を監査・修正
  4. アイランドの認証境界を確認 — ページミドルウェアのみに依存していないか確認
  5. アイランド未使用時は 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.componentIslandstrue または { localOnly: true } になっているか。デフォルト値はバージョンによって異なる。

確認方法A: nuxt.config.ts の直接確認(ホワイトボックス)

# プロジェクトルートで実行
grep -rn "componentIslands" nuxt.config.ts nuxt.config.js nuxt.config.mjs

結果判定:

確認方法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

結果判定:

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

結果判定:


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

結果判定:


Step 4: アイランドコンポーネントのunsafe HTML sink確認

判定基準: アイランドコンポーネントがユーザー入力(props)を v-htmlinnerHTML でレンダリングしているか。

確認方法A: ソースコード検索(ホワイトボックス・推奨)

# アイランドディレクトリ内を検索
grep -rn "v-html\|innerHTML\|dangerouslySetInnerHTML" components/islands/

# または、すべてのアイランドコンポーネント(.server.vue)を検索
grep -rn "v-html\|innerHTML" components/**/*.server.vue

結果判定:

確認方法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 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 EverythingCache Deception ARMOR を有効化
Vercel vercel.json の headers Cache-Control: public, s-maxage=N にクエリパラメータを含める
CloudFront Cache Behavior Cache key and cache policiesQueryString を含める
Nginx proxy_cache_key $scheme$proxy_host$request_uri(クエリ含む)
Fastly VCL vcl_hash req.url 全体(クエリ含む)をハッシュキーに