v-htmlの甘美な毒:Vue.jsにおけるDOMベースXSSの深層と防衛アーキテクチャ
現場のコードレビューで、v-htmlを目にした瞬間に眉をひそめる。それがセキュリティアーキテクトとしての反射だ。Vue.jsはデフォルトでテンプレート内のデータをエスケープし、XSS(Cross-Site Scripting)に対して極めて堅牢な防壁を築いている。しかし、開発者が「リッチテキストをレンダリングしたい」という誘惑に負け、v-htmlという名のパンドラの箱を開けた瞬間、その防壁は砂上の楼閣と化す。
今日は、Vue.jsにおけるv-htmlの脆弱性を単なる「警告」で終わらせず、メモリレイヤの挙動とブラウザのパーサ仕様、そして生成AI時代の「信頼境界」という観点から解体していく。
—
1. なぜv-htmlは脆弱性の「特異点」なのか
Vue.jsのテンプレートエンジンは、データバインディングにおいて値をプレーンテキストとして扱う。これは、DOMツリーが構築される際に、値が解析対象(パース)ではなく、単純なtextContentとして扱われることを意味する。
しかし、v-htmlディレクティブを使用すると、Vueは内部的にinnerHTMLプロパティを呼び出す。ここでブラウザのパーサが起動する。攻撃者が注入した文字列が、HTML構造として解釈されるわけだ。
攻撃のメカニズム:パーサの「勘違い」を突く
攻撃者は、単なるタグを挿入するような古典的手法は使わない。WAFや単純な正規表現フィルタを回避するために、以下のようなペイロードを送り込む。
ブラウザのHTMLパーサは、src属性が解決できない場合にonerrorイベントをトリガーする。この挙動は仕様(HTML5 Spec)であり、JavaScriptエンジンのランタイムにおいて、攻撃者が任意のスクリプトを実行する「正当な権限」を与えてしまう。
---
2. 実装レベルでの防衛:サニタイザの強制介入
「v-htmlを絶対に使わない」のが理想だが、CMSやMarkdownエディタなど、どうしてもHTMLレンダリングが必要な場面はある。その際、我々アーキテクトが講じるべきは「入力の信頼性をゼロと見なす」というゼロトラスト設計だ。
堅牢なサニタイズ・アーキテクチャ
DOMの構築前に、プロトコルや属性をホワイトリストで絞り込む必要がある。ここでは、業界標準であるDOMPurifyをVueのカスタムディレクティブとしてラップする手法を推奨する。
// main.js - セキュアなHTMLレンダリングのためのカスタムディレクティブ定義
import DOMPurify from 'dompurify';
const vSafeHtml = {
mounted(el, binding) {
// 注入前にDOMPurifyで悪意ある要素を徹底的に除去
// ALLOWED_TAGSとALLOWED_ATTRを最小限に絞るのがコツ
const cleanHtml = DOMPurify.sanitize(binding.value, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href', 'title']
});
el.innerHTML = cleanHtml;
},
updated(el, binding) {
// 更新時も同様にサニタイズを適用
el.innerHTML = DOMPurify.sanitize(binding.value);
}
};
// アプリケーション全体でグローバル登録
app.directive('safe-html', vSafeHtml);
この実装により、v-htmlという「生の爆弾」を、許可されたタグのみを通過させる「フィルタリング・ゲートウェイ」へと昇華させる。
---
3. 生成AI時代の新たな脅威:プロンプト・インジェクションの連鎖
今、我々が直面しているのは、単なるXSSだけではない。LLM(大規模言語モデル)が出力したHTMLコンテンツをそのままv-htmlに流し込むケースだ。
もしLLMの出力が攻撃者によって汚染されていたら?あるいはLLM自体がハルシネーション(幻覚)によって、攻撃用ペイロードを生成してしまったら?
アーキテクチャ上のガードレイル
1. 分離層(Isolation)の構築: AIの出力結果を直接DOMにレンダリングせず、一度中継サーバを経由させ、サンドボックス化されたiframe内でレンダリングする。
2. CSP(Content Security Policy)の適用: v-htmlの脆弱性を補完する最後の防壁がCSPだ。
HTTPレスポンスヘッダの例
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';
script-src 'self'を設定すれば、外部ドメインからのスクリプト実行を物理的に遮断できる。たとえコードにバグがあっても、ブラウザ側で実行を拒否させる。これこそが、多層防御の極致だ。
---
4. 最後に:コードは書くものではなく、削るもの
セキュリティとは、何かを付け加えることではない。「必要のない機能をいかに殺すか」という引き算の芸術だ。v-htmlを使わない実装が可能なら、それが最もコストパフォーマンスの高いセキュリティ対策となる。
テックリードとして、チームに伝えるべきは「動けばいい」という甘い考えを捨て、「なぜ動いてしまうのか」というメモリレイヤの挙動に思いを馳せる文化を根付かせることだ。
セキュリティはツールで解決するものではない。設計思想そのものだ。次回のコードレビューで、v-htmlを見かけたら、こう問いかけてほしい。「そのHTML、本当にブラウザで直接パースする必要があるか?」と。その問いが、あなたのプロダクトを次のインシデントから救うことになるはずだ。
コメント