`empty-results` 属性のHTML注入脆弱性をテキスト代入で修正
LexicalPromptElementの空結果表示メソッドで、empty-results属性値がinnerHTMLへ直接代入されていた脆弱性をtextContentへの切り替えによって修正しました。ホストアプリケーションがユーザー入力をempty-results属性に反映している場合、DOMベースのHTML注入が可能な状態でした。
背景
empty-results属性を介したHTML注入が、HackerOne経由(#3709373)で報告されました。問題の核心は、LexicalPromptElement#showEmptyResultsメソッドが#emptyResultsMessageの値をinnerHTMLに渡していた点にあります。
たとえば、サーバーサイドテンプレートで次のように属性を構成しているホストアプリは攻撃対象になり得ます。
<lexxy-prompt empty-results="No match for <%= params[:q] %>">
この場合、params[:q]に<img src=x onerror=alert(1)>のようなペイロードを含めると、ポップオーバーのDOM内で実行されます。strict CSPが適用されていてもスクリプト実行は防げますが、<meta http-equiv="refresh">を使ったオープンリダイレクトは依然として可能でした。同種の問題は以前にも<action-text-attachment>のcontent属性で発生しており、#903でDOMPurifyによるサニタイズで対処されています。今回の修正は同じ脆弱性パターンの別箇所への適用です。
技術的な変更
src/elements/prompt.jsの#showEmptyResultsメソッドで、createElementに渡すプロパティをinnerHTMLからtextContentに1行変更しました。
変更前:
#showEmptyResults() {
this.popoverElement.classList.add("lexxy-prompt-menu--empty")
const el = createElement("li", { innerHTML: this.#emptyResultsMessage })
el.classList.add("lexxy-prompt-menu__item--empty")
this.popoverElement.append(el)
}
変更後:
#showEmptyResults() {
this.popoverElement.classList.add("lexxy-prompt-menu--empty")
const el = createElement("li", { textContent: this.#emptyResultsMessage })
el.classList.add("lexxy-prompt-menu__item--empty")
this.popoverElement.append(el)
}
リグレッションテストとしてtest/browser/fixtures/prompt-empty-results.htmlとtest/browser/tests/prompts/empty_results.test.jsが追加されました。フィクスチャにはonerror="window.__xss = true"を含む<img>タグをそのまま埋め込んだempty-results属性が設定されており、テストは次の3点を検証します。
- ポップオーバー内に
<img>要素や<b>要素が注入されていないこと - 属性値全体がリテラルテキストとしてそのまま表示されること
-
window.__xssがtrueにならないこと(onerrorハンドラが実行されていないこと)
デフォルトのNOTHING_FOUND_DEFAULT_MESSAGEはプレーンテキストであるため、既存の動作に変化はありません。
設計判断
DOMPurifyによるサニタイズではなく、textContentへの切り替えが採用されました。
empty-results属性は「検索結果が空のときに表示するメッセージ」を表す文字列属性であり、HTMLマークアップを含む用途は意図されていません。#903でcontent属性に使われたDOMPurifyによるサニタイズはリッチコンテンツ(<span>を含むメンション等)を保持する必要があったため適切でしたが、今回は属性の意味的な役割からしてテキストとして扱うのが自然です。textContentへの代入はHTMLパーサーを経由しないため、サニタイズルールの抜け漏れという問題自体が発生しません。
まとめ
1文字の変更(innerHTML→textContent)で、属性値の意味に沿った扱いに修正しつつHTML注入の経路を根本的に排除しています。サニタイズで防ぐ方針と異なり、そもそもHTMLとして解釈させないアプローチを選んだことで、将来的なサニタイズ漏れのリスクも回避しています。