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 |
<invalid>foo<p>bar</p>bazz</invalid><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:
:stripkeeps 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 <script src="malicious.js">this & 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> <foo>but foo is <b>not</b></foo>"
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
:escapeturns an HTML comment into visible<!-- ... -->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
hrefand images lose theirsrc.<a href="https://example.com">x</a>becomes<a>x</a>. Do not use:whitewashif 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
- Link and Text Scrubbers
- Safelist Reference
- Internals
- Rails Helpers:
Loofah::Helpers.sanitizeuses:strip.