`table_for` のカラムヘッダーに渡したHTMLが正しくレンダリングされるよう修正
table_for のソート可能なカラムヘッダーにHTMLコンテンツを渡した際、SVGなどのタグが生テキストとして出力されてしまう問題を修正しました。あわせて、文字列結合時に発生していたセキュリティ上の懸念も解消されています。
背景
table_for のカラムヘッダーにSVGアイコンなどのHTMLコンテンツを渡すと、ソート機能のあるカラムで意図しない表示になる問題がありました。たとえば以下のようにアイコンを列タイトルとして渡す場合です:
svg = content_tag :svg, width: '20', height: '20' do
content_tag :use, nil, href: image_path("icons/key.svg#main")
end
column(svg, sortable: :access_level) { |r| r.access_level }
このケースでは、ソートアイコン用のSVGをタイトルに結合する処理の中で、カラムタイトルに渡されたHTML文字列がエスケープされずに html_safe を呼び出す形になっていたため、ソートアイコンのSVGが生のHTMLテキストとして表示されてしまっていました。
技術的な変更
問題は lib/active_admin/views/components/table_for.rb の build_table_header メソッドにおける文字列結合処理にありました。
変更前:
svg = '<svg class="data-table-sorted-icon" fill="none" viewBox="0 0 10 6"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/></svg>'
(col.pretty_title + svg).html_safe
変更後:
svg = '<svg class="data-table-sorted-icon" fill="none" viewBox="0 0 10 6"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/></svg>'.html_safe
safe_join([col.pretty_title, svg])
変更のポイントは2点です。まず、ソートアイコン用のSVGリテラル文字列に対して直接 .html_safe を呼び出し、開発者がその文字列を安全なものとして明示的にマークするようにしました。次に、タイトルとSVGの結合を文字列の + 演算子と後付けの .html_safe から、safe_join を使った結合に置き換えています。
safe_join はRailsが提供するヘルパーで、配列の各要素を個別にエスケープ処理したうえで結合します。html_safe な要素はそのまま保持し、そうでない要素はHTMLエスケープされるため、col.pretty_title がHTMLコンテンツを含む場合でも正しく展開されます。
テスト面では spec/unit/views/components/table_for_spec.rb に新たなコンテキストが追加され、<span> タグをカラムタイトルに渡した際にHTMLとして展開されること、かつソートアイコンの <svg> も正しくレンダリングされることを検証しています。
設計判断
safe_join を使った結合方式 が選ばれた点が、この修正の核心です。
変更前の (col.pretty_title + svg).html_safe という記述は、結合後の文字列全体に html_safe を付与するパターンです。この方法では、col.pretty_title がユーザー入力由来の文字列であった場合にエスケープが失われるリスクがありました。一方、safe_join はRailsのビューヘルパーが推奨するイディオムであり、各要素のHTML安全性を個別に評価したうえで結合するため、意図しないXSSリスクを抑制できます。
SVGリテラルに .html_safe を先に付与したうえで safe_join に渡す構成にすることで、「このSVGは信頼できる固定文字列である」という意図が明示的になり、コードの可読性と安全性が向上しています。
まとめ
本PRは、文字列結合後に html_safe を付与するアンチパターンを safe_join による安全な結合に置き換えることで、HTMLコンテンツをカラムヘッダーに渡せるようにしつつ、意図しないXSSリスクの解消も同時に達成した修正です。変更箇所は2行のみですが、Railsにおけるビュー層のHTML安全性を扱うイディオムの正しい使い方を示す好例といえます。