動かざることバグの如し

近づきたいよ 君の理想に

安易にhtml_safe使うな safe_join使え

環境

  • Rails 8.0

safe_joinサンプルコード

Railsのビューで複数の文字列をHTMLタグで結合したい場面がある。たとえば複数行のテキストを<br>区切りで表示したいとき、素朴に書くとこうなる。

lines.join("<br>").html_safe

これで動くにはするが、html_safeを使うと全体のHTMLエスケープが無効になり、linesの中にユーザー入力が混入していると XSS の温床になる。そこで使うのがsafe_joinだ。

module ApplicationHelper
  def make_sample_lines
    lines = []
    lines << "りんご"
    lines << "バナナ"
    lines << "ぶどう"
    safe_join(lines, tag.br)
  end
end
<p><%= make_sample_lines %></p>

safe_join使うメリットとは? html_safeでもよくね?

safe_join の核心はここだ。各要素を個別にエスケープしつつ、セパレータのHTMLはそのまま通すという挙動にある。

lines = ["<script>alert('xss')</script>", "バナナ"]
safe_join(lines, tag.br)
# => "&lt;script&gt;alert('xss')&lt;/script&gt;<br>バナナ"

html_safeを使った場合と比較してみる。

# NG: 全体が unsafe 扱いのまま
lines.join(tag.br)

# NG: html_safe で全体のエスケープを無効化 → XSSリスク
lines.join("<br>").html_safe

# OK: 各要素はエスケープ、セパレータはそのまま出力
safe_join(lines, tag.br)

html_safeを一度つけてしまうと「この文字列は安全だとRailsが信用する」という意味になり、その中にユーザー入力が含まれていても無条件にHTMLとして出力される。一方safe_joinは要素ごとにエスケープ処理を適用するため、セパレータにtag.brのようなHTMLタグを渡しても安全に扱える。

つまり「動けばいい」ならhtml_safeでも通るが、ユーザー入力や外部データを扱うならsafe_join一択だ。

参考リンク