Avoid html_safe with Tag Helpers, safe_join, and sanitize
When you need to build HTML outside of a template, it’s tempting to concatenate strings and call html_safe on the result. This bypasses Rails’s built-in XSS protection entirely: any user input in that string goes straight to the browser unescaped.
The good news is you almost never need html_safe. Rails provides three underappreciated tools that handle escaping for you.
Instead of…
…calling html_safe on strings you’ve built by hand:
def status_badge(label, color)
"<span class=\"badge badge-#{color}\">#{label}</span>".html_safe
end
def formatted_address(user)
[user.street, user.city, user.postcode].compact.join("<br>").html_safe
end
def render_comment(comment)
comment.body_html.html_safe
end
Use…
…the right tool for each situation.
When you need to build HTML elements, use tag helpers:
def status_badge(label, color)
tag.span(label, class: "badge badge-#{color}")
end
The tag helper escapes the content and attributes automatically. It returns an HTML-safe string without you having to think about it.
When you need to join fragments that mix safe HTML with potentially unsafe text, use safe_join:
def formatted_address(user)
safe_join([user.street, user.city, user.postcode].compact, tag.br)
end
safe_join escapes any unsafe strings in the array and returns an HTML-safe result. It’s Array#join with protection built in.
When you need to accept user-provided HTML but strip dangerous tags, use sanitize:
def render_comment(comment)
sanitize(comment.body_html)
end
sanitize keeps safe tags like <p>, <strong>, and <em> while stripping <script>, event handlers, and other XSS vectors. You can customise the allowed tags and attributes, but beware of straying past the defaults — they are battle-tested and loosening them is a footgun.
Why?
Each of these tools lets Rails manage HTML safety for you. You describe what you want — an element, a joined list, sanitised content — and the framework handles the escaping.
html_safe does the opposite. It tells Rails “trust this string, don’t escape it”. That’s a promise you have to keep, and it’s easy to break when the inputs change or a future developer doesn’t realise user data flows through that path. Ask your friendly security consultant or penetration testing organisation why this is a bad idea.
The mental model is simple. Need an HTML element? tag. Joining fragments? safe_join. Accepting rich text? sanitize. If none of those fit, you probably need a partial or a component, not a string.
Why not?
There are legitimate uses of html_safe. Some gems, like pagy, return pre-built HTML strings that are safe by construction. Calling html_safe on their output is fine because the gem controls the content.
You might also see html_safe on strings that are genuinely static with no user input, like " ".html_safe. That’s harmless, but you can include the actual Unicode character instead — "\u00A0" gives you a non-breaking space without needing html_safe at all. That is ugly as hell though, so it’s your call!
The key question is always: could user input end up in this string? If the answer is yes, or even maybe, reach for tag, safe_join, or sanitize instead.
Published on May 4th, 2026