コンテンツにスキップ

Storefront リッチテキスト描画と sanitized HTML

  • 作成日: 2026-05-22
  • 関連実装:
  • apps/storefront/src/lib/safe-html.tsx(sanitizer + React レンダラ)
  • apps/storefront/src/lib/safe-html.test.tsx
  • scripts/quality/check-storefront-rich-text-rendering.mjs(CI lint)
  • package.jsonlint:storefront-rich-text script
  • 関連コミット: 46ba4cb4b(カタログ collection description を safe HTML 描画へ), 1fdf91842(rich text 検証 lint 追加)

背景

商品説明、コレクション説明、CMS 由来コンテンツなど、Storefront に流れる「リッチテキストフィールド」は Vendure / WordPress / SMILE のいずれの上流でも生 HTML が混入し得る。文字列をそのまま dangerouslySetInnerHTML に渡すと stored XSS の入口になり、逆に常にテキストとして描画すると改行 / 強調 / リンク等のフォーマットを失う。

本仕様は Storefront 側で リッチテキストを sanitized HTML として描画する唯一の入口を定義し、CI で違反を検出する。

描画ルール

  • リッチテキストプロパティ(body / content / description / html 等)は safe-html.tsx の sanitizer 経由でのみ 描画する。
  • 信頼モデルは default-allow。 trust boundary は「WP / Vendure 管理画面にログインできる社内編集者」で閉じている前提のため、タグ / 属性の許可リストは維持せず、CMS 由来 HTML を基本的に信頼して描画する。<iframe>(Vimeo/YouTube 等、ホスト制限なし)・<video> / <audio> / <source> / <track><embed> / <object><form> / <input>style="..." インライン属性などはそのまま通る。新しい埋め込みが増えてもサニタイザ更新は不要。
  • 描画価値がなく XSS / hijack の入口にしかならない最小限の要素だけを BLOCKED_TAGS / 属性安全網として常に除去する:
  • <script> / <base> タグ、SMIL 経由で javascript: を注入し得る SVG タグ(animate / animateTransform / set / foreignObject / use)。
  • on* イベントハンドラ属性、<iframe>srcdoc 属性。
  • URL 属性・全属性値の javascript: / vbscript: / data:text/html スキーム(http(s): / mailto: / tel: / 相対のみ許可)。
  • <style> ブロックはページ全体を壊し得るため、allowCmsStyles 経由で sanitizeCmsCss() 除毒 + scope 化した場合のみ許可(詳細は apps/storefront/AGENTS.md)。
  • 対応する CSP(storefront-runtime-policy.ts)は frame-src 'self' https:media-src 'self' https: blob: を許可し、object-src 'none' を維持する。
  • 画像 src は resolveStorefrontHtmlImageAttributes で正規化し、外部リンクは rel="noopener noreferrer" を付与する。
  • 生文字列をそのまま {description} のように JSX へ埋めるパターンは 禁止。フォーマットが失われ、かつ「上流が突然 HTML を返したとき」に挙動が壊れる。
  • 匿名ユーザー入力(コメント / レビュー等)を描画する用途が将来できた場合、上記 default-allow の信頼前提が崩れるため SafeHtml をそのまま流用せず別の厳格なサニタイズ経路を設けること。

CI lint

  • scripts/quality/check-storefront-rich-text-rendering.mjs が AST 走査でリッチテキスト系プロパティの直書きを検出する。
  • 監視対象プロパティ名: body, content, description, htmlrichTextPropertyNames)。
  • 例外は同スクリプトの allowedDirectTextExpressionsファイルパス + 理由 を併記して登録する(component chrome copy 等、上流が CMS / Vendure ではないと確証できる場合のみ)。
  • pnpm run lintlint:storefront-rich-text を含むため、PR の必須 gate に組み込まれている。

例外を増やさない運用

  • 「component が呼び出し側から固定文字列を受け取るだけ」を理由に例外登録するときは、その component が CMS / Vendure / SMILE からデータを直接受けない不変性を、呼び出し側の型定義 or 命名で保証すること。
  • 例外を追加するときは PR 説明に「この path が CMS / Vendure 由来でないことの根拠」を 1 文で残す。

テスト

  • apps/storefront/src/lib/safe-html.test.tsx で:
  • <script> / <base> / on* ハンドラ / javascript: URL / <style>(非許可時)が除去されること
  • 信頼する埋め込み(任意 https <iframe>、自己ホスト <video controls>)が要素として描画されること
  • 通常タグ(a, p, strong, ul, ol, li, img 等)の再構成、画像 src 正規化、外部リンクの rel 付与が機能すること
  • カタログ collection description 経路の挙動は products-page-content.test.tsx / article-detail-page-content.test.tsx でカバーされる。

関連ガイド

  • docs/03-implementation/storefront-style-guide.md
  • docs/specifications/product-detail-product-scope.md