Vue I18n: `escapeParameter` が翻訳メッセージ内の `srcdoc` 属性による XSS 攻撃を完全に防ぐことはできません。
### 概要
`escapeParameter` が有効になっている場合(レガシー API の `escapeParameterHtml`)、Vue I18n は翻訳された文字列を `sanitizeTranslatedHtml()` を通して処理します。このサニタイザーは、属性値内の `<` と `>` をエスケープし、`onerror` などのイベントハンドラ属性を無効化しますが、`<iframe>` タグの `srcdoc` 属性は処理しません。
`srcdoc` の値は HTML ドキュメントであり、ブラウザはそれに含まれる文字参照をデコードし、結果を同じオリジン内のネストされたドキュメントとして解析します。サニタイザーによるエスケープと無効化はどちらも文字参照であるため、`srcdoc` 内ではこれらの処理が解除されます。これにより、CVE-2025-53892 (GHSA-x8qp-wqqm-57ph) の対策が回避されます。
### 詳細
`sanitizeTranslatedHtml()` ([v11.4.13 の `packages/shared/src/sanitize.ts`](https://github.com/intlify/vue-i18n/blob/v11.4.13/packages/shared/src/sanitize.ts#L126-L162)) は、最終的な翻訳された文字列に対して以下の処理を行います。
1. 引用符で囲まれた属性値内の `<`, `>`, `"` および `'` をエスケープします。
2. イベントハンドラ属性の `on` を `on` に書き換えます。
3. `href`, `src`, `action` および `formaction` 内の `javascript:` URL を `about:blank` に置き換えます。
`srcdoc` 内では、上記の手順 1 と 2 は文字参照のみを生成し、ブラウザは値をドキュメントとして解析する前にこれらの文字参照をデコードします。したがって、`srcdoc` 内のマークアップは、エスケープの有無にかかわらず、サニタイザーを通過します。
この問題は、以下の3つの方法で発生する可能性があります。
- **`srcdoc` 内のパラメータ。** `<iframe srcdoc="{content}"></iframe>` のようなメッセージの場合、`escapeParameter` が `content` を文字参照に変換し、ブラウザがそれを再度デコードします。
- **メッセージ内のマークアップ。** `<iframe srcdoc="...">` を含むメッセージは、サニタイズ後も保持されます。ドキュメントでは、ユーザーからのコンテンツを含む HTML メッセージの使用を避けるように指示されていますが、同時に、サニタイザーが最終的な HTML 内のイベントハンドラを無効化するとも記載されています。
- **GHSA-8rc2-428q-7ww3 との関連。** 呼び出しでリストと名前付きの値が渡される場合、名前付きの値はエスケープされません。名前付きの値に含まれる `<iframe srcdoc="...">` は、サニタイザーにマークアップとして渡されます。例えば、`{0} {name}` のような単純なメッセージでも同様です。
### 実証コード
```js
import { createI18n } from 'vue-i18n'
const i18n = createI18n({
legacy: false,
locale: 'en',
escapeParameter: true,
messages: {
en: { message: '<iframe srcdoc="<b>x</b>"></iframe>' }
}
})
console.log(i18n.global.t('message'))
// v11.4.13: <iframe srcdoc="<b>x</b>"></iframe>
```
ブラウザは値をデコードし、`<b >x</b >` を `<iframe>` 内の別のドキュメントとしてレンダリングします。この位置にあるマークアップは、すべて同じように解析されます。
### 影響
アプリケーションは、`escapeParameter` を有効にし、翻訳を HTML としてレンダリングする(例えば `v-html` を使用して)場合に、以下のいずれかの条件が満たされると影響を受けます。
- 信頼できない値が、メッセージによって `srcdoc` 内に配置されるパラメータとして渡される。
- メッセージの内容が完全に信頼できない(例えば、クラウドソーシングされた翻訳や保存された翻訳)。
- 信頼できない名前付きの値が、リストと一緒に同じ呼び出しで渡される(GHSA-8rc2-428q-7ww3)。
攻撃者は、サニタイザーがあっても、アプリケーションのオリジンでスクリプトを実行できます(DOM ベースの XSS)。
`<i18n-t>` を使用したレンダリングは影響を受けません。なぜなら、翻訳を HTML としてレンダリングしないからです。
### 修正
バージョン11.4.14と12.0.0-alpha.5で修正されました。サニタイザーは、イベントハンドラー属性と同様に、`srcdoc`属性自体を無効化するようになりました。
バージョン9およびバージョン10はサポートを終了しており、修正は行われません。バージョン11への移行をお願いします。
### 対策
- 信頼できないコンテンツが含まれる可能性のある翻訳を`v-html`でレンダリングしないようにしてください。代わりに、コンポーネントの補間 (`<i18n-t>`) を使用してください。
- 翻訳メッセージに`srcdoc`を含めないでください。また、`srcdoc`に値を渡さないでください。
### 参照
- [CWE-79](https://cwe.mitre.org/data/definitions/79.html), [CWE-116](https://cwe.mitre.org/data/definitions/116.html)
- [GHSA-x8qp-wqqm-57ph](https://github.com/intlify/vue-i18n/security/advisories/GHSA-x8qp-wqqm-57ph) (CVE-2025-53892): `sanitizeTranslatedHtml()`を追加した修正
- [GHSA-8rc2-428q-7ww3](https://github.com/intlify/vue-i18n/security/advisories/GHSA-8rc2-428q-7ww3): リストが渡された場合、名前付きの値はエスケープされません。
- [HTML Standard: the `srcdoc` attribute](https://html.spec.whatwg.org/multipage/iframe-embed-object.html#attr-iframe-srcdoc)
影響を受けるパッケージ:
- npm (JavaScript) vue-i18n: 影響を受けるバージョン = 9.14.5、修正バージョン:なし
- npm (JavaScript) vue-i18n: 影響を受けるバージョン = 10.0.8、修正バージョン:なし
- npm (JavaScript) vue-i18n: 影響を受けるバージョン >= 11.1.10、< 11.4.14、修正バージョン:11.4.14
- npm (JavaScript) vue-i18n: 影響を受けるバージョン = 12.0.0-alpha.4、修正バージョン:12.0.0-alpha.5
- npm (JavaScript) petite-vue-i18n: 影響を受けるバージョン = 9.14.5、修正バージョン:なし
- npm (JavaScript) petite-vue-i18n: 影響を受けるバージョン = 10.0.8、修正バージョン:なし
- npm (JavaScript) petite-vue-i18n: 影響を受けるバージョン >= 11.1.10、< 11.4.14、修正バージョン:11.4.14
- npm (JavaScript) petite-vue-i18n: 影響を受けるバージョン = 12.0.0-alpha.4、修正バージョン:12.0.0-alpha.5
- npm (JavaScript) @intlify/core-base: 影響を受けるバージョン = 9.14.5、修正バージョン:なし
- npm (JavaScript) @intlify/core-base: 影響を受けるバージョン = 10.0.8、修正バージョン:なし
- npm (JavaScript) @intlify/core-base: 影響を受けるバージョン >= 11.1.10、< 11.4.14、修正バージョン:11.4.14
- npm (JavaScript) @intlify/core-base: 影響を受けるバージョン = 12.0.0-alpha.4、修正バージョン:12.0.0-alpha.5
- npm (JavaScript) @intlify/core: 影響を受けるバージョン = 9.14.5、修正バージョン:なし
- npm (JavaScript) @intlify/core: 影響を受けるバージョン = 10.0.8、修正バージョン:なし
- npm (JavaScript) @intlify/core: 影響を受けるバージョン >= 11.1.10、< 11.4.14、修正バージョン:11.4.14
- npm (JavaScript) @intlify/core: 影響を受けるバージョン = 12.0.0-alpha.4、修正バージョン:12.0.0-alpha.5
- npm (JavaScript) @intlify/vue-i18n-core: 影響を受けるバージョン = 9.14.5、修正バージョン:なし
- npm (JavaScript) @intlify/vue-i18n-core: 影響を受けるバージョン = 10.0.8、修正バージョン:なし
- npm (JavaScript) @intlify/vue-i18n-core: 影響を受けるバージョン >= 11.1.10、< 11.4.14、修正バージョン:11.4.14
- npm (JavaScript) @intlify/vue-i18n-core: 影響を受けるバージョン = 12.0.0-alpha.4、修正バージョン:12.0.0-alpha.5
- npm (JavaScript) @intlify/shared: 影響を受けるバージョン = 9.14.5、修正バージョン:なし
- npm (JavaScript) @intlify/shared: 影響を受けるバージョン = 10.0.8、修正バージョン:なし
- npm (JavaScript) @intlify/shared: 影響を受けるバージョン >= 11.1.10、< 11.4.14、修正バージョン:11.4.14
- npm (JavaScript) @intlify/shared: 影響を受けるバージョン = 12.0.0-alpha.4、修正バージョン:12.0.0-alpha.5