Link and Text Scrubbers

Besides the four sanitizers, Loofah includes built-in scrubbers for common, non-security transformations: hardening links, removing problematic Unicode characters, and splitting paragraphs. None of them removes unsafe markup, so combine them with a sanitizing scrubber when the input is untrusted.

Scrubbers on this page:

How the rel Scrubbers Combine Values

:nofollow, :noopener, and :noreferrer all use Loofah::Scrubber#append_attribute, which:

  1. reads the current rel value (or an empty string if there is none),
  2. splits it on whitespace,
  3. adds the new value only if it is not already present,
  4. writes the values back joined by single spaces.

So existing values are preserved, duplicates are not created, and the scrubbers can be chained in any order:

html = '<a href="http://www.example.com/" rel="noopener">Click here</a>'
Loofah.html5_fragment(html).scrub!(:nofollow).to_s
# => "<a href=\"http://www.example.com/\" rel=\"noopener nofollow\">Click here</a>"

All three only look at <a> elements. Other elements that can carry rel or point somewhere, such as <area> or <form>, are not modified. They also do not check href: an <a> without href still gets the rel value.

:nofollow

Property Value
Class Loofah::Scrubbers::NoFollow
Traversal top-down
Affects Every <a> element
Effect Appends nofollow to rel

rel="nofollow" tells search engines not to pass ranking credit through the link, which removes the incentive for link spam in comments.

html = "ohai! <a href='http://www.myswarmysite.com/'>I like your blog post</a>"
Loofah.html5_fragment(html).scrub!(:nofollow).to_s
# => "ohai! <a href=\"http://www.myswarmysite.com/\" rel=\"nofollow\">I like your blog post</a>"

:noopener

Property Value
Class Loofah::Scrubbers::NoOpener
Traversal top-down
Affects Every <a> element
Effect Appends noopener to rel

rel="noopener" prevents a page opened through the link from accessing window.opener.

html = '<a href="http://www.example.com/" rel="nofollow">Click here</a>'
Loofah.html5_fragment(html).scrub!(:noopener).to_s
# => "<a href=\"http://www.example.com/\" rel=\"nofollow noopener\">Click here</a>"

:noreferrer

Property Value
Class Loofah::Scrubbers::NoReferrer
Traversal top-down
Affects Every <a> element
Effect Appends noreferrer to rel

rel="noreferrer" stops the browser from sending the Referer header when the link is followed.

html = '<a href="http://www.example.com/">Click here</a>'
Loofah.html5_fragment(html).scrub!(:noreferrer).to_s
# => "<a href=\"http://www.example.com/\" rel=\"noreferrer\">Click here</a>"

:targetblank

Property Value
Class Loofah::Scrubbers::TargetBlank
Traversal top-down
Affects <a> elements with an href that does not start with #
Effect Sets target="_blank", replacing any existing target

:targetblank makes links open in a new tab or window. Unlike the rel scrubbers it overwrites an existing value:

Loofah.html5_fragment('<a href="http://www.example.com/" target="_top">Click here</a>')
  .scrub!(:targetblank).to_s
# => "<a href=\"http://www.example.com/\" target=\"_blank\">Click here</a>"

Links that are skipped:

  • In-page anchors, whose href starts with # (since version 2.24.0):

    Loofah.html5_fragment('<a href="#top">Click here</a>').scrub!(:targetblank).to_s
    # => "<a href=\"#top\">Click here</a>"
  • <a> elements without an href attribute.

The class comment notes that on modern browsers target="_blank" implicitly behaves as if rel="noopener" were set. If you also support older browsers, chain :noopener as well.

:unprintable

Property Value
Class Loofah::Scrubbers::Unprintable
Traversal top-down
Affects Text and CDATA nodes
Effect Removes U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR)

Despite its name, this scrubber removes exactly two characters: U+2028 and U+2029. They are invisible in most renderings but were historically illegal inside JavaScript string literals, so content containing them could break when embedded in a <script> block or parsed as JavaScript.

html = "<b>Lo
ofah ro
cks!</b><script>x
y</script>"
Loofah.html5_fragment(html).scrub!(:unprintable).to_s
# => "<b>Loofah rocks!</b><script>xy</script>"

Note from this example that :unprintable is not a sanitizer: the <script> element survives. It also does not touch attribute values or other control characters.

:double_breakpoint

Property Value
Class Loofah::Scrubbers::DoubleBreakpoint
Traversal bottom-up
Affects <p> elements containing two consecutive <br> children
Effect Splits the paragraph into several <p> elements at each <br><br>

Content from some editors uses <br><br> inside a single paragraph to fake paragraph breaks. :double_breakpoint turns those into real paragraphs:

html = "<p>Some text here in a logical paragraph.<br><br>Some more text, apparently a second paragraph.<br><br>Et cetera...</p>"
Loofah.html5_fragment(html).scrub!(:double_breakpoint).to_s
# => "<p>Some text here in a logical paragraph.</p><p>Some more text, apparently a second paragraph.</p><p>Et cetera...</p>"

Detailed behavior, from the project's test cases:

Input Output Rule
<p>a<br><br>b</p> <p>a</p><p>b</p> Basic split
<p>a<br/><br/>b</p> <p>a</p><p>b</p> Self-closing <br/> works
<p>a<br> <br>b</p> <p>a</p><p>b</p> Whitespace-only text between the breaks is ignored
<p>a <br><br> b</p> <p>a </p><p> b</p> Whitespace inside surrounding text is kept
<p>x <b>y</b><!-- c --><br><br><i>z</i> w</p> <p>x <b>y</b><!-- c --></p><p><i>z</i> w</p> Inline elements and comments are kept
<p>a<br><br><br><br>b</p> <p>a</p><p></p><p>b</p> Each pair splits, which can create empty paragraphs
<p>a<br><br><br>b</p> <p>a</p><p><br>b</p> An odd <br> is kept
<p><br><br>a</p> <p></p><p>a</p> A leading pair leaves an empty paragraph
<p class="c">a<br><br>b</p> <p>a</p><p>b</p> Attributes of the original <p> are not copied
<p>a<br>b</p> unchanged A single <br> is left alone
<p>a<br><!-- c --><br>b</p> unchanged Breaks separated by a comment do not count
<p>a<br>&nbsp;<br>b</p> unchanged &nbsp; is not whitespace for this purpose
<p><b>a<br><br>b</b> <i>c</i></p> unchanged Breaks inside an inline child are not direct children
<div><p>a<br><br>b</p></div> <div><p>a</p><p>b</p></div> Nested paragraphs are processed

When you call it on a node, only paragraphs inside that node change:

doc = Loofah.html5_document("<html><body><p>a<br><br>b</p><p>c<br><br>d</p></body></html>")
doc.at_css("p").scrub!(:double_breakpoint)
doc.at_xpath("/html/body").inner_html
# => "<p>a</p><p>b</p><p>c<br><br>d</p>"

Note: The scrubber rebuilds a paragraph whenever it has a <br> child followed (anywhere later) by another <br> child, even if the two are not adjacent. In that case the paragraph is recreated without its attributes and its whitespace-only text children are dropped, although no split happens. This follows from the implementation; the tests only cover paragraphs without attributes in this situation.

Note: The CHANGELOG's unreleased section records a fix for this scrubber: before it, :double_breakpoint left document fragments unchanged despite its documented example. With version 2.25.2 and earlier, apply it to a document, or upgrade to the release that contains the fix.

:newline_block_elements

Property Value
Class Loofah::Scrubbers::NewlineBlockElements
Traversal bottom-up
Affects Block-level elements and <br>
Effect Replaces each with a text node

This scrubber is marked :nodoc: and exists to implement to_text. It replaces every element in Loofah::Elements::LINEBREAKERS with plain text:

  • <br> becomes "\n";
  • any other block-level element becomes "\n" + its text content + "\n".

It destroys markup (all of a block element's descendants are flattened to their text), so you rarely want to call it directly. to_text runs it on a copy of your document, leaving the original untouched.

See Also