`empty-results` 属性のHTML注入脆弱性をテキスト代入で修正

basecamp/lexxy

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.htmltest/browser/tests/prompts/empty_results.test.jsが追加されました。フィクスチャにはonerror="window.__xss = true"を含む<img>タグをそのまま埋め込んだempty-results属性が設定されており、テストは次の3点を検証します。

  • ポップオーバー内に<img>要素や<b>要素が注入されていないこと
  • 属性値全体がリテラルテキストとしてそのまま表示されること
  • window.__xsstrueにならないこと(onerrorハンドラが実行されていないこと)

デフォルトのNOTHING_FOUND_DEFAULT_MESSAGEはプレーンテキストであるため、既存の動作に変化はありません。

設計判断

DOMPurifyによるサニタイズではなく、textContentへの切り替えが採用されました。

empty-results属性は「検索結果が空のときに表示するメッセージ」を表す文字列属性であり、HTMLマークアップを含む用途は意図されていません。#903content属性に使われたDOMPurifyによるサニタイズはリッチコンテンツ(<span>を含むメンション等)を保持する必要があったため適切でしたが、今回は属性の意味的な役割からしてテキストとして扱うのが自然です。textContentへの代入はHTMLパーサーを経由しないため、サニタイズルールの抜け漏れという問題自体が発生しません。

まとめ

1文字の変更(innerHTMLtextContent)で、属性値の意味に沿った扱いに修正しつつHTML注入の経路を根本的に排除しています。サニタイズで防ぐ方針と異なり、そもそもHTMLとして解釈させないアプローチを選んだことで、将来的なサニタイズ漏れのリスクも回避しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
59a109a1

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)」という構成が明確で、理想的なマクロ構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(`javascript:src/elements/prompt.js`)やPR番号のリンク記法(`[#903](...)`)が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

HTML注入やDOMの挙動に関する知識を前提としており、専門のエンジニアという対象読者に適切です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論で構成され、各パラグラフがトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られており非常に読みやすいです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

Diffで示された`innerHTML`から`textContent`への変更を、変更前後のコードブロックで正確に引用しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`innerHTML`, `textContent`, `DOMPurify`, `CSP`などの技術用語が、文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

innerHTMLがHTMLをパースすることに起因する脆弱性と、textContentがテキストとして扱うことでそれを防ぐという説明は、技術的に正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

HackerOneのレポート番号や関連PR番号など、記事内のすべての主張がPR情報で裏付けられており、ハルシネーションは見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#1035)、関連PR番号(#903)、HackerOneレポート番号(#3709373)がすべて正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRの主題「HTML injection fix」を正確に反映し、かつ「テキスト代入で修正」という具体的な解決策を加えており、より分かりやすくなっています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容はすべて提供されたPR情報に基づいており、バージョンサポート状況などのPR外の知識を持ち込んでいません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

過去の脆弱性(「〜でした」)や過去の修正(「〜で対処されています」)など、時間的な前後関係を正しく表現しています。