`table_for` のカラムヘッダーに渡したHTMLが正しくレンダリングされるよう修正

activeadmin/activeadmin

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.rbbuild_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安全性を扱うイディオムの正しい使い方を示す好例といえます。

記事メタデータ

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

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

ファイル名付きシンタックスハイライト(```ruby:ファイルパス)とGitHubのPRリンク記法([#9013](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

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

`safe_join`や`html_safe`といった用語を前提知識として扱っており、専門知識を持つエンジニアという対象読者に適合した内容になっています。

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

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

各セクションが総論→各論の構造を持ち、各パラグラフがトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が非常によく守られています。これにより、記事の骨子が掴みやすくなっています。

Diff内容との照合 ✓ PASS

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

記事内のコードブロックは、提供されたDiff情報を正確に反映しています。変更前後のコード引用、ファイル名ともに一致しており、テストファイルへの言及も正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`html_safe`, `safe_join`, `XSS`, `エスケープ`といった技術用語が正確かつ適切な文脈で使用されています。「アンチパターン」「イディオム」といった表現も、Rails開発の文脈において適切です。

説明の技術的正確性 ✓ PASS

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

`safe_join`の挙動や、文字列結合と`html_safe`呼び出しに伴うセキュリティリスクに関する説明は技術的に正確で、論理的です。

事実の突合 ⚠ WARNING

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

記事の主張の大部分はPR情報で裏付けられていますが、「safe_joinがRailsのヘルパーである」という説明や、それが「イディオム」であるという評価はPRに明記されていません。ただし、これらは変更を解説するための妥当な補足情報です。

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

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

PR番号(#9013)やファイルパスなど、記事に含まれる数値や固有名詞はすべて正確です。

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

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

記事のタイトル「`table_for` のカラムヘッダーに渡したHTMLが正しくレンダリングされるよう修正」は、PRのタイトル「Allow supplied HTML in table_for column header」の内容を正確に反映しています。

外部知識の正確性 ⚠ WARNING

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

`safe_join`がRailsのヘルパーであることや、それが一般的なベストプラクティス(イディオム)であるという説明は、PR外の知識です。しかし、PRの変更内容を理解する上で不可欠な情報であり、捏造ではありません。

時間表現の正確性 ✓ PASS

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

記事内に時間表現に関する記述はなく、PR情報との不一致もありません。