DOMPurifyの許可リスト漏れによりアンダーライン書式が消失していた問題を修正

basecamp/lexxy

エディタでアンダーライン書式を適用しても、値の出力時にHTMLサニタイザーが <u> タグを除去していたバグが修正されました。src/config/dom_purify.js の許可タグリストに "u" を追加する1行の修正で、書式の往復変換(round-trip)が正しく機能するようになります。

背景

Cmd+U(macOS)または Ctrl+U(Windows)でアンダーラインを適用すると、エディタのDOM上では <span class="lexxy-content__underline"> としてスタイルが反映され、エクスポート層が <u> タグに変換していました。しかし value の取得時に DOMPurify による sanitize() が走り、許可リストにない <u> タグが除去されて、アンダーライン情報が失われた状態でHTML文字列が返されていました。

この問題は #710 として報告されています。ユーザーがアンダーライン書式を適用して保存・再読み込みを行うと、エディタはアンダーラインを認識しない素のテキストとして表示していました。エクスポート層(text_node_export_helper.js)の実装は正しかったにもかかわらず、その下流のサニタイズ処理が結果を破壊するという、レイヤー間の設定不整合が根本原因です。

技術的な変更

変更は src/config/dom_purify.jsALLOWED_HTML_TAGS 配列への "u" の追加1件と、リグレッションテストの追加2件のみです。

変更前:

const ALLOWED_HTML_TAGS = [ "a", "b", "blockquote", "br", "code", "div", "em",
  "figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "li", "mark", "ol", "p", "pre", "q", "s", "strong", "ul", "table", "tbody", "tr", "th", "td" ]

変更後:

const ALLOWED_HTML_TAGS = [ "a", "b", "blockquote", "br", "code", "div", "em",
  "figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "li", "mark", "ol", "p", "pre", "q", "s", "strong", "u", "ul", "table", "tbody", "tr", "th", "td" ]

テストは test/browser/tests/formatting/inline_formatting.test.js に2ケースが追加されています。1つ目はキーボードショートカットによるアンダーライン適用後に <u> タグが出力されることを検証し、2つ目は setValue("<p>Hello <u>everyone</u></p>")value の往復変換でタグが保持されることを確認します。

test("underline via keyboard shortcut", async ({ page, editor }) => {
  await editor.setValue(HELLO_EVERYONE)
  await editor.select("everyone")

  const modifier = process.platform === "darwin" ? "Meta" : "Control"
  await editor.content.press(`${modifier}+u`)
  await assertEditorHtml(editor, "<p>Hello <u>everyone</u></p>")
})

test("underline round-trips through setValue/value", async ({ editor }) => {
  await editor.setValue("<p>Hello <u>everyone</u></p>")
  await assertEditorHtml(editor, "<p>Hello <u>everyone</u></p>")
})

すでに許可されている "s"(打ち消し線)・"b" / "i" と同様のパターンで追加されており、許可リストの整合性が取られています。

設計判断

エクスポート層とサニタイズ設定を分離して管理する構造が採用されているため、今回のような「実装は正しいが設定が追いついていない」ミスが生じやすいトレードオフが存在します。

リッチテキストエディタにおいてHTMLサニタイズは必須ですが、エディタが生成するタグセットと許可リストは常に同期が必要です。今回のPRでは許可リストへの追加のみで修正が完結しており、エクスポート層の実装変更は不要でした。これは、エクスポート層の設計自体は正しかったことを示しています。

テストが setValue/value の往復変換を明示的に検証する形で追加されたことで、今後の許可リスト変更や新しい書式要素の追加時に同種のリグレッションを検出できる安全網が整備されました。

まとめ

エクスポート層とHTMLサニタイズ設定の間に生じていたタグ許可漏れという単純な設定ミスが、ユーザーには「書式が保存されない」という深刻な問題として現れていた例です。往復変換テストの追加により、今後は同種の設定不整合をCI上で早期に検出できる体制が整いました。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
e6735c7d

この記事は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リンク記法の正確性

ファイル名付きのシンタックスハイライト(```言語:ファイルパス)や、GitHubのPR・Issueへのリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

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

「DOMPurify」「HTMLサニタイザー」「round-trip」などの専門用語が適切に使用されており、専門知識を持つエンジニアという対象読者に適合しています。

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

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

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

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードは、提供されたDiff情報と完全に一致しています。ファイル名も正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

PR Descriptionで使われている「allowlist」「sanitizer」「round-trip」などの技術用語が、記事内で正確に反映・使用されています。

説明の技術的正確性 ✓ PASS

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

エクスポート層とサニタイズ処理のレイヤー間の問題という、変更の技術的な背景がPR情報に基づいて正確に説明されています。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのDescriptionやDiff内のコードで裏付けられており、ハルシネーション(捏造)は見られません。

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

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

PR番号(#866)やIssue番号(#710)が正確に記載・リンクされています。

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

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

記事のタイトルはPRの主題「underline formatting not persisting」を的確に要約し、原因である「DOMPurifyの許可リスト漏れ」まで踏み込んでおり、非常に優れています。

外部知識の正確性 ✓ PASS

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

バージョンサポート状況やリリース日程など、PR情報に基づかない外部知識の追記はなく、信頼性が保たれています。

時間表現の正確性 ✓ PASS

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

「すでに許可されている」などの時間表現がPR情報と一致しており、事実関係を正確に伝えています。