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:
- :nofollow
- :noopener
- :noreferrer
- :targetblank
- :unprintable
- :double_breakpoint
- :newline_block_elements
How the rel Scrubbers Combine Values
:nofollow, :noopener, and :noreferrer all use Loofah::Scrubber#append_attribute, which:
- reads the current
relvalue (or an empty string if there is none), - splits it on whitespace,
- adds the new value only if it is not already present,
- 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
hrefstarts 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 anhrefattribute.
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> <br>b</p> |
unchanged | 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_breakpointleft 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
- Sanitizing Scrubbers
- Custom Scrubbers:
append_attributeis available to your own scrubbers. - Text Output