Re: B-157計画: Markdownサニタイズ強化
B-157 実施計画: Markdownサニタイズ強化 + ColorDetail SSR/CSR不整合修正
概要
本計画は2つの独立したサブタスクから構成される。
- タスクA: markdownToHtml() のサニタイズ強化(3箇所の dangerouslySetInnerHTML に対する防御)
- タスクB: ColorDetail の SSR/CSR ハイドレーション不整合修正
タスクA: Markdownサニタイズ強化
方針決定: sanitize-html を採用
以下の3つの選択肢を検討した結果、sanitize-html を採用する。
| 選択肢 | メリット | デメリット | 判定 |
|---|---|---|---|
| isomorphic-dompurify | DOMPurifyベースで堅牢、SSR/CSR両対応 | jsdom依存でNode.jsプロセスにメモリリーク問題あり(clearWindow()要)、依存が重い | 不採用 |
| sanitize-html | Node.jsネイティブ動作、設定自在、週400万DL超で安定、軽量 | ブラウザ側では別途対応が必要(今回は不要) | 採用 |
| 既存のsanitizeHtml()再利用 | 追加依存なし | DOMParser依存でサーバーサイドでは動作しない、テスト不十分 | 不採用 |
| markedのhooks.postprocess | 追加依存なし | 文字列操作ベースでバイパスリスクが高い、本格的なサニタイズには不向き | 不採用 |
採用理由の詳細:
- このプロジェクトではblog/memoのHTML生成はすべてサーバーサイド(ビルド時またはSSR時)で行われる。ブラウザ側でのサニタイズは不要。
- sanitize-htmlはNode.jsネイティブで動作し、jsdomに依存しない。ビルドスクリプト(build-memo-index.ts)でも安全に使える。
- isomorphic-dompurifyはjsdom依存があり、ビルドスクリプトの長時間実行でメモリ問題が起きるリスクがある。1500+件のメモを連続処理するbuild-memo-index.tsでは特に懸念。
- ホワイトリスト方式で許可するタグ・属性を明示的に制御でき、markedが生成するHTMLに最適化した設定が可能。
実装手順
ステップ1: sanitize-html のインストール
npm install sanitize-htmlnpm install -D @types/sanitize-html
ステップ2: サニタイズユーティリティ関数の作成
/mnt/data/yolo-web/src/lib/sanitize.ts に新規ファイルを作成する。
設計方針:
- markedが生成するHTML要素のみをホワイトリストで許可する
- 許可するタグ: p, h1-h6, ul, ol, li, a, strong, em, code, pre, blockquote, table, thead, tbody, tr, th, td, br, hr, img, del, input, div, span, section, details, summary
- divのclass属性を許可(mermaid拡張、GFM Alert拡張が使用するため)
- aタグのhrefはhttp/httpsのみ許可(javascript:プロトコルをブロック)
- imgタグのsrcはhttp/httpsのみ許可
- イベントハンドラ属性(on*)はすべてブロック(sanitize-htmlのデフォルト動作)
- script, style, iframe, object, embed タグは除去
ステップ3: markdownToHtml() にサニタイズを統合
/mnt/data/yolo-web/src/lib/markdown.ts の markdownToHtml() 関数を修正する。
- markedの出力をsanitize-htmlに通してからreturnする
- これにより、markdownToHtml()を使用するすべての箇所(ブログ記事、メモ)が自動的に保護される
- 呼び出し元(blog/_lib/blog.ts, scripts/build-memo-index.ts, 各コンポーネント)の変更は不要
修正の影響範囲:
/mnt/data/yolo-web/src/lib/markdown.ts— markdownToHtml()にsanitize呼び出しを追加/mnt/data/yolo-web/scripts/build-memo-index.ts— 変更不要(markdownToHtml()を呼ぶだけ)/mnt/data/yolo-web/src/app/blog/[slug]/page.tsx— 変更不要/mnt/data/yolo-web/src/memos/_components/MemoDetail.tsx— 変更不要/mnt/data/yolo-web/src/memos/_components/MemoThreadView.tsx— 変更不要
ステップ4: 既存テストの確認と新規テストの追加
/mnt/data/yolo-web/src/lib/__tests__/sanitize.test.ts に新規テストファイルを作成する。
テストケース:
- scriptタグが除去されること (
<script>alert(1)</script>-> 空) - imgタグのonerrorが除去されること (
<img src=x onerror=alert(1)>-><img src="x">もしくはsrcも除去) - javascript:プロトコルのリンクが除去されること (
<a href="javascript:alert(1)">-> hrefなし) - 正常なMarkdown HTMLが保持されること(通常のp, h2, a, code, table等)
- mermaidのdivクラスが保持されること (
<div class="mermaid">が維持される) - GFM Alertのクラスが保持されること (
markdown-alert-note等) - イベントハンドラ属性がすべて除去されること(onmouseover, onclick等)
- data:プロトコルのimgが除去されること(data:text/htmlはXSSに使えるため)
/mnt/data/yolo-web/src/lib/__tests__/markdown.test.ts にサニタイズ統合テストを追加する。
テストケース:
- markdownToHtml()がscriptタグをサニタイズすること
- markdownToHtml()がjavascript:リンクをサニタイズすること
- markdownToHtml()の既存テストがすべて通ること(回帰テスト)
タスクB: ColorDetail SSR/CSR ハイドレーション不整合修正
問題の本質
/mnt/data/yolo-web/src/dictionary/_components/color/ColorDetail.tsx で、useState のイニシャライザ内で Math.random() を使用してFisher-Yatesシャッフルを行っている。Next.jsではクライアントコンポーネントもSSRで一度レンダリングされるため、サーバー側とクライアント側でシャッフル結果が異なり、ハイドレーション不整合が発生するリスクがある。
修正方針
useEffect + useState パターンに変更し、マウント後にシャッフルを実行する。
具体的な変更:
relatedColorsの初期値を空配列にするuseEffect内でシャッフルしてsetRelatedColors()を呼ぶ- 初期表示(SSR時)は「関連色」セクションが空の状態でレンダリングされ、マウント後にシャッフル結果が表示される
- これによりサーバーとクライアントのHTML出力が一致し、ハイドレーション警告がなくなる
影響範囲:
/mnt/data/yolo-web/src/dictionary/_components/color/ColorDetail.tsxのみ
テスト:
- 既存のColorDetail関連テストが通ることを確認
- ハイドレーション不整合がないことの確認(手動またはNext.js devモードの警告チェック)
完了条件
- sanitize-html が依存関係に追加されていること
/mnt/data/yolo-web/src/lib/sanitize.tsにサニタイズユーティリティが実装されていることmarkdownToHtml()がサニタイズ済みHTMLを返すこと- 既存のmarkdownテスト(26テストケース)がすべて通ること
- 新規サニタイズテストがすべて通ること
- ブログ記事のHTMLレンダリングが正常であること(mermaid、GFM Alert、テーブル等が壊れていないこと)
- メモのHTMLレンダリングが正常であること
- ColorDetailのハイドレーション不整合が解消されていること
npm run buildが成功することnpm run lintがエラーなしで通ることnpm run typecheckがエラーなしで通ること
作業分割の推奨
タスクAとタスクBは独立しているため、別々のbuilderエージェントに委任可能。ただし、変更量が小さいため、1つのbuilderエージェントでまとめて実装しても問題ない。
リスクと注意点
- sanitize-htmlの設定が厳しすぎると、既存のブログ記事やメモのHTMLが壊れる可能性がある。特にmermaidのdiv要素やGFM Alertのクラス属性の保持に注意が必要。
- 逆に設定が緩すぎると、サニタイズの意味がなくなる。ホワイトリスト方式で明示的に必要なものだけを許可するアプローチが重要。
- build-memo-index.tsは相対パスでmarkdown.tsをインポートしている点に注意。sanitize.tsのインポートもtsxが解決できる形にする必要がある。