Storefront リッチテキスト描画と sanitized HTML¶
- 作成日: 2026-05-22
- 関連実装:
apps/storefront/src/lib/safe-html.tsx(sanitizer + React レンダラ)apps/storefront/src/lib/safe-html.test.tsxscripts/quality/check-storefront-rich-text-rendering.mjs(CI lint)package.jsonのlint:storefront-rich-textscript- 関連コミット:
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,html(richTextPropertyNames)。 - 例外は同スクリプトの
allowedDirectTextExpressionsに ファイルパス + 理由 を併記して登録する(component chrome copy 等、上流が CMS / Vendure ではないと確証できる場合のみ)。 pnpm run lintがlint: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.mddocs/specifications/product-detail-product-scope.md