Skip to content

Instantly share code, notes, and snippets.

@joekur
Last active October 17, 2024 15:14
Show Gist options
  • Save joekur/73779c40c481a2f8a44f to your computer and use it in GitHub Desktop.
Save joekur/73779c40c481a2f8a44f to your computer and use it in GitHub Desktop.
Proper Use of `html_safe`

Proper use of html_safe

Let's look at an innocuous piece of ruby. Consider some view code showing a user's name and phone number:

"#{first_name} #{last_name} #{phone}"

Great - this is very succinct, readable, and can easily be extracted to a method in a decorator/presenter type object. But let's build on this - consider now that we want to make that phone number a link. Simple enough right?

"#{first_name} #{last_name} #{link_to(phone, 'tel:'+phone)}"

Let's drop that into the page and see what we get...

Johnny Appleseed <a href="tel:1231231234">1231231234</a>

Ew! What happened? Looks like the view escaped the html produced by the link_to method. How do we get around that? Well, why don't we just tell it not to escape:

"#{first_name} #{last_name} #{link_to(phone, 'tel:'+phone)}".html_safe

WAIT! This is very Bad (with a capital B which rhymes with C which stands for Cross-Site Scripting). If anyone sticks malicious input into their first or last name, then anywhere you display this to a user, that user is susceptible to all sorts of XSS attacks.

Here's a safe way to write this:

"".html_safe + "#{first_name} #{last_name} " + link_to(phone, 'tel:'+phone)

This may look a little strange, but let's break down what's happening.

Firstly we create an empty ActiveSupport::SafeBuffer. A SafeBuffer is like a string that we have marked as being safe to be rendered without escaping. The html_safe method simply turns a String into a SafeBuffer. This part seems like it would be unnecessary, but we'll get to that.

Next, we add it to a String - "Johnny Appleseed " in our example. When you add a SafeBuffer to a String, the String first gets html escaped, and then the two are concatenated. The result is another SafeBuffer.

Lastly we are adding our combined SafeBuffer to the result of link_to. The link_to method itself returns a SafeBuffer (as does all rails helpers that produce html). This is why we can use this helper in views and the resulting tag is rendered correctly. Two SafeBuffers together is just another SafeBuffer, no more escaping needed.

So why the need for the empty SafeBuffer at the beginning? Because if you are concatenating instead a String to a SafeBuffer, the String does NOT get escaped, and you end up with a String instead.

You can easily test how your Rails view will escape your embedded ruby by using the underlying method - ERB::Util#html_escape. Here's proof of my previous statement:

string_first = "<script>foo</script>" + "<p>Hello</p>".html_safe
# => "<script>foo</script><p>Hello</p>"
string_first.class
# => String
ERB::Util.html_escape string_first
# => "&lt;script&gt;foo&lt;/script&gt;&lt;p&gt;Hello&lt;/p&gt;"

safe_buffer_first = "<p>Hello</p>".html_safe + "<script>foo</script>"
# => "<p>Hello</p>&lt;script&gt;foo&lt;/script&gt;"
safe_buffer_first.class
# => ActiveSupport::SafeBuffer
ERB::Util.html_escape safe_buffer_first
# => "<p>Hello</p>&lt;script&gt;foo&lt;/script&gt;"

Notice how the second example escapes the String but not the SafeBuffer. Again, if you were to drop the first example into your view, since it is not marked as safe, Rails will escape the entire String, p tag and everything.

This does mean that you can't use ruby's string interpolation to concatenate Strings and SafeBuffers together, as it will always return a String that will be entirely escaped in your view.

Just remember, always pause before you throw in an html_safe and make sure you understand what you're doing. Other than calling it on an emptry string like we did above, you almost never have to use it, since rails provides helpers such as tag and content_tag that will automatically create html-safe elements and escape their contents for you.

@ocher
Copy link

ocher commented Oct 29, 2019

Very good explanation. 👏

@masa-ekohe
Copy link

👍

@gathuku
Copy link

gathuku commented Feb 23, 2022

🎉

@Splines
Copy link

Splines commented Dec 11, 2023

As an alternative to this, one could use the sanitize method provided by the TextHelper, see the documentation, especially the sanitize method which will sanitize but not escape your HTML content (as far as I understood).

You/People might also find these links interesting

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment