Quick Start

This tutorial takes you from an empty Ruby file to a small, realistic sanitization pipeline: parse untrusted HTML, remove dangerous markup, add link attributes, apply a custom transformation, and produce a plain-text preview. Each step shows the expected result.

The examples use the HTML5 parser. If Loofah.html5_support? is false on your platform (for example on JRuby), replace html5 with html4 in every method name; see Installation.

1. Parse a Fragment

User-submitted HTML is almost always a fragment: a piece of markup without <html>, <head>, or <body>. Parse it with Loofah.html5_fragment:

require "loofah"

input = <<~HTML
  <p>Hello <b>world</b>!</p>
  <script>alert('pwned')</script>
  <p onclick="steal()">Click <a href="javascript:alert(1)">here</a>
  or <a href="https://example.com">there</a>.</p>
HTML

fragment = Loofah.html5_fragment(input)
fragment.class # => Loofah::HTML5::DocumentFragment

Nothing has been removed yet. fragment is a Nokogiri fragment with Loofah's extra methods, so you can already query it:

fragment.css("a").map { |a| a["href"] }
# => ["javascript:alert(1)", "https://example.com"]

2. Sanitize It

Call scrub! with one of the built-in sanitizers. :prune removes unsafe elements together with everything inside them:

fragment.scrub!(:prune)
puts fragment.to_s

The output is similar to:

<p>Hello <b>world</b>!</p>

<p>Click <a>here</a>
or <a href="https://example.com">there</a>.</p>

What happened:

  • <script> is not on the safelist, so it was removed with its content.
  • onclick is not an allowed attribute, so it was removed from the <p>.
  • href="javascript:alert(1)" failed the URI check, so the href attribute was removed (the <a> element itself is allowed and stays).

scrub! modifies the fragment in place and returns it, so calls can be chained.

Chain link scrubbers to harden the remaining links:

fragment.scrub!(:nofollow).scrub!(:noopener)
fragment.css("a").map(&:to_html)
# => ["<a rel=\"nofollow noopener\">here</a>",
#     "<a href=\"https://example.com\" rel=\"nofollow noopener\">there</a>"]

The rel values are appended rather than overwritten, and a value already present is not duplicated. See Link and Text Scrubbers.

4. Write a Custom Scrubber

Suppose your site styles bold text with <strong> instead of <b>. A scrubber is a block that receives every node:

b_to_strong = Loofah::Scrubber.new do |node|
  node.name = "strong" if node.name == "b"
end

fragment.scrub!(b_to_strong)
fragment.at_css("strong").to_html # => "<strong>world</strong>"

Custom Scrubbers explains traversal order, Loofah::Scrubber::STOP, and class-based scrubbers.

5. Produce Plain Text

For notification emails or previews, convert the fragment to text. to_text places newlines around block elements and <br>:

Loofah.html5_fragment("<h1>Title</h1><div>Content<br>Next line</div>").to_text
# => "\nTitle\n\nContent\nNext line\n"

Compare with text, which just concatenates text nodes:

Loofah.html5_fragment("<h1>Title</h1><div>Content<br>Next line</div>").text
# => "TitleContentNext line"

Both escape <, >, and & by default so the result is safe to drop into HTML. See Text Output for the encode_special_chars option.

6. Use the One-Line Shorthand

For the common "parse and scrub" case there are shorthand methods:

Loofah.scrub_html5_fragment(input, :prune).to_s

This is exactly Loofah.html5_fragment(input).scrub!(:prune).

Putting It Together

require "loofah"

class CommentSanitizer
  B_TO_STRONG = Loofah::Scrubber.new do |node|
    node.name = "strong" if node.name == "b"
  end

  def self.html(input)
    Loofah.html5_fragment(input)
      .scrub!(:prune)
      .scrub!(:nofollow)
      .scrub!(:noopener)
      .scrub!(B_TO_STRONG)
      .to_s
  end

  def self.preview(input)
    Loofah.html5_fragment(input).scrub!(:prune).to_text.strip
  end
end

Note: Always run a sanitizing scrubber (:prune, :strip, :escape, or :whitewash) before relying on the output being safe. The link scrubbers and custom scrubbers do not remove anything dangerous on their own.

Next Steps