Sanitizing Scrubbers

Loofah ships four sanitizing scrubbers. All four use the same allowlist (Loofah::HTML5::SafeList) to decide which elements are safe; they differ in what happens to everything else. This page describes each one precisely, compares them side by side, and helps you pick.

Scrubbers on this page:

What They Have in Common

:strip, :prune, and :escape share one decision procedure for each node they visit:

Node type Decision
Element on the safelist Kept. Its attributes are cleaned (see below) and its children are visited.
Element not on the safelist Unsafe, handled by the scrubber's strategy.
Text node Kept.
CDATA node (and, on JRuby, text inside <style>) Its <, >, and & are escaped; see Internals.
Anything else: comments, processing instructions, and so on Unsafe, handled by the scrubber's strategy.

Attribute cleaning on allowed elements removes attributes that are not on the safelist, attributes holding unsafe URIs (such as javascript:), unsafe CSS declarations inside style, and attributes left empty. data-* attributes are always kept. The Safelist Reference and URI and CSS Sanitization pages list the details.

The safelist includes common formatting and structure elements (p, div, span, a, img, table, headings, lists), MathML, and SVG, as well as html, head, and body. It does not include script, style, iframe, object, embed, link, meta, frame, or frameset. (title is allowed, because it is also an SVG element name.)

Note: Form-related elements (form, input, button, select, textarea, option, label, fieldset) are on the safelist. If you do not want forms in user content, remove them yourself; see Removing elements the safelist allows.

Comparison

Using the input from the project's integration tests:

<invalid>foo<p>bar</p>bazz</invalid><div>quux</div>
Scrubber Result Direction
:strip foo<p>bar</p>bazz<div>quux</div> bottom-up
:prune <div>quux</div> top-down
:escape &lt;invalid&gt;foo&lt;p&gt;bar&lt;/p&gt;bazz&lt;/invalid&gt;<div>quux</div> top-down
:whitewash Like :prune, and also removes every attribute top-down

:strip

Property Value
Class Loofah::Scrubbers::Strip
Traversal bottom-up
Unsafe element Replaced by its children
Unsafe non-element (comment, ...) Removed

:strip removes the unsafe tag but keeps its content. Because it runs bottom-up, the children have already been scrubbed by the time the parent is unwrapped, so content nested deep inside an unknown tag is still sanitized.

unsafe_html = "ohai! <div>div is safe</div> <foo>but foo is <b>not</b></foo>"
Loofah.html5_fragment(unsafe_html).scrub!(:strip).to_s
# => "ohai! <div>div is safe</div> but foo is <b>not</b>"

Use :strip when the content of unknown tags is valuable text, for example markup from an editor that invents custom tags.

Warning: :strip keeps the text inside a <script> or <style> element. The tag is gone and the text is inert (and escaped), but the reader will see it:

Loofah.html4_fragment("<script>alert('evil')</script><span>w00t</span>").scrub!(:strip).to_s
# => "alert('evil')<span>w00t</span>"

If you do not want script source in your output, use :prune.

Nested script tags are handled too; for example, the test suite checks that <script><script src="malicious.js">this & that</script> stripped with HTML4 serializes as &lt;script src="malicious.js"&gt;this &amp; that.

:prune

Property Value
Class Loofah::Scrubbers::Prune
Traversal top-down
Unsafe element Removed with its entire subtree
Unsafe non-element (comment, ...) Removed

:prune deletes the unsafe node and everything inside it. Since it runs top-down and stops at the removed node, children of a pruned element are never visited.

unsafe_html = "ohai! <div>div is safe</div> <foo>but foo is <b>not</b></foo>"
Loofah.html5_fragment(unsafe_html).scrub!(:prune).to_s
# => "ohai! <div>div is safe</div> "

:prune is the safest general-purpose choice for user-generated content: nothing from an unknown element leaks into the output. The trade-off is that legitimate text wrapped in an unrecognized tag disappears.

:escape

Property Value
Class Loofah::Scrubbers::Escape
Traversal top-down
Unsafe element Replaced by a text node containing its serialized HTML
Unsafe non-element (comment, ...) Replaced by a text node containing its serialized markup

:escape turns the unsafe node (and its whole subtree) into visible text. When serialized, the text shows the original markup with entity-escaped angle brackets:

unsafe_html = "ohai! <div>div is safe</div> <foo>but foo is <b>not</b></foo>"
Loofah.html5_fragment(unsafe_html).scrub!(:escape).to_s
# => "ohai! <div>div is safe</div> &lt;foo&gt;but foo is &lt;b&gt;not&lt;/b&gt;&lt;/foo&gt;"

Note that the <b> inside <foo> is escaped as well: the whole subtree becomes text, even the safe parts. Use :escape when users should see exactly what they typed, such as a forum where people paste HTML snippets that should be displayed rather than rendered.

Note: Comments are not on the safelist either, so by the same rule :escape turns an HTML comment into visible &lt;!-- ... --&gt; text. This follows from the code path (comments fall through to the "unsafe" branch) rather than from a dedicated test.

:whitewash

Property Value
Class Loofah::Scrubbers::Whitewash
Traversal top-down
Allowed element without namespace Kept, with all attributes removed
Allowed element with a namespace (for example o:p) Removed with its subtree
Unsafe element Removed with its subtree
Comment and other non-text nodes Removed
CDATA node Escaped copy inserted, original removed

:whitewash is the most aggressive cleaner. It prunes like :prune, then strips every attribute from the elements that remain, including class, id, style, href, and src, and removes namespaced elements.

messy_markup = "ohai! <div id='foo' class='bar' style='margin: 10px'>div with attributes</div>"
Loofah.html5_fragment(messy_markup).scrub!(:whitewash).to_s
# => "ohai! <div>div with attributes</div>"

Its intended use case is markup pasted from Microsoft Word into a rich-text editor, which is full of namespaced o: elements, conditional comments, and inline styles:

input = "<o:div>no</o:div><div id='no'>foo</div><invalid>bar</invalid><!--[if gts mso9]><div>microsofty stuff</div><![endif]-->"
doc = Loofah.html5_document("<html><body>#{input}</body></html>").scrub!(:whitewash)
doc.at_xpath("/html/body").inner_html
# => "<div>foo</div>"

Warning: Because every attribute is removed, links lose their href and images lose their src. <a href="https://example.com">x</a> becomes <a>x</a>. Do not use :whitewash if you need working links or images.

How namespaced tags are parsed depends on the parser, so the result for <o:div> varies. For the input above, the project's tests expect <div>foo</div> with the HTML5 parser and with libxml2 older than 2.9.11 or 2.10.4 and newer; <o:div>no</o:div><div>foo</div> on JRuby; and <div>no</div> followed by a newline and <div>foo</div> with libxml2 versions from 2.9.11 up to (but not including) 2.10.4.

Choosing a Sanitizer

You want... Use
Safe output, losing unknown content entirely :prune
Safe output, keeping the text of unknown tags :strip
Users to see the raw HTML they typed :escape
Plain structural markup with no attributes at all :whitewash
Only plain text Any of the above, then text or to_text (Text Output)

All four can be combined with the link and text scrubbers and with your own custom scrubbers. Run the sanitizer first, so that later scrubbers only see safe markup, unless your custom scrubber is meant to rescue content (for example converting a custom tag to a safe one) before sanitization would remove it.

See Also