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
# => "<script>foo</script><p>Hello</p>"
safe_buffer_first = "<p>Hello</p>".html_safe + "<script>foo</script>"
# => "<p>Hello</p><script>foo</script>"
safe_buffer_first.class
# => ActiveSupport::SafeBuffer
ERB::Util.html_escape safe_buffer_first
# => "<p>Hello</p><script>foo</script>"
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.
Very good explanation. 👏