Open-source project
rgrove/sanitize avatar
rgrove/sanitize

Sanitize: the Ruby sanitizer that deletes everything you did not allowlist

Ruby HTML and CSS sanitizer.

2,057 stars143 forksRubyMIT

At a glance

What is it?
Sanitize is an allowlist-based Ruby library for HTML and CSS that parses with Nokogiri and Crass, strips every tag when you pass no config, and warns twice that MathML and SVG cannot be cleaned safely.
Who is it for?
Adopt Sanitize when user-supplied HTML or CSS reaches a Ruby app and you want every keep-or-drop decision written into an allowlist instead of a pattern list; MIT licensing makes it easy to embed. Do not adopt it for MathML or SVG, and do not route CSS that will end up inside a page through `Sanitize::CSS.stylesheet`.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • rgrove/sanitize2,057 ★ · +0
Star history
Repository2026-10-142026-10-23
rgrove/sanitize2.1k2.1k

0 Stars in the last 9 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

With no config Sanitize strips every tag and keeps the text

Sanitize is an allowlist sanitizer for Ruby: it removes every piece of HTML or CSS from a string except the elements, attributes, properties, at rules and URL protocols you name. That is the whole contract, and it runs opposite to a regex filter or a blocklist. A pattern filter decides what to look for and leaves everything it did not match alone. Sanitize parses first, then rebuilds the output from the allowlist, and it has no memory of anything you did not write down. Markup goes through the Nokogiri HTML5 parser and CSS goes through Crass, both of which parse the way modern browsers do.

With no configuration at all the settings are the strictest available, and the practical result is that all markup disappears and only safe text survives. One sample input produces two different results depending on whether you name a config at all:

ruby
html = '<b><a href="http://foo.com/">foo</a></b><img src="bar.jpg">'
Sanitize.fragment(html)
# => "foo"

That sample string runs through the whole documentation, and the result deserves a close look. The default strips the bold element along with the link and the image, while the named `Sanitize::Config::RESTRICTED` config returns `<b>foo</b>` from identical input. So the unnamed default is stricter than the mildest named config, and passing no config is not the same thing as passing RESTRICTED. Teams that reach for RESTRICTED expecting the plain default get slightly more markup than they asked for.

Allowlisting elements, attributes and CSS through one option hash

Allowlisting is cumulative and explicit. An `elements` array names tags, an `attributes` hash maps each tag to the attributes it may keep, a `css` sub-hash takes properties and at rules, and protocol allowlists sit beside the attributes that carry URLs. Anything missing from those lists is removed, including elements that would otherwise be harmless. Adding one element is a one-line change:

ruby
Sanitize.fragment(html, elements: ['b'])
# => "<b>foo</b>"

The CSS side follows the same rule at a finer grain, because CSS has to be cleaned separately from the markup that carries it. To keep a style element or a style attribute you first allowlist the element or the attribute, then allowlist the properties inside it. In the documented example the properties list holds one name, and the output shows what that single choice costs:

ruby
Sanitize.fragment(html,
  elements: ['div', 'style'],
  attributes: {'div' => ['style']},

  css: {
    properties: ['width']
  }
)

The result keeps the div element and its style attribute, keeps a 1024 pixel width inside the style element and a 100 pixel width in the attribute, and drops height and color because neither name is in the properties allowlist. The paragraph element is missing from the element allowlist too, so its text survives and its tags do not. Options arrive as a trailing keyword hash on each call instead of a config object, which keeps a per-call override to one line and makes each call site readable on its own.

4 entry points cover 6 kinds of input

Sanitize exposes 4 entry points and covers 6 kinds of input: HTML fragments, HTML documents, CSS in style elements, CSS in style attributes, standalone stylesheets, and standalone property lists. `Sanitize.fragment` takes a snippet with no root-level html element, and `Sanitize.document` takes a whole page. `Sanitize::CSS.stylesheet` and `Sanitize::CSS.properties` take CSS on its own, and those same two paths run when a fragment or a document carries CSS. All four accept a config, and the bundled configs are plain Ruby constants handed over as the second argument:

ruby
require 'sanitize'

# Clean up an HTML fragment using Sanitize's permissive but safe Relaxed config.
# This also sanitizes any CSS in `<style>` elements or `style` attributes.
Sanitize.fragment(html, Sanitize::Config::RELAXED)

# Clean up an HTML document using the Relaxed config.
Sanitize.document(html, Sanitize::Config::RELAXED)

# Clean up a standalone CSS stylesheet using the Relaxed config.
Sanitize::CSS.stylesheet(css, Sanitize::Config::RELAXED)

# Clean up some CSS properties using the Relaxed config.
Sanitize::CSS.properties(css, Sanitize::Config::RELAXED)

The narrowest of the four is `Sanitize::CSS.properties`, which takes a bare declaration list such as `left: expression(alert('xss!'));` and returns only the declarations the allowlist kept. None of these calls returns an error object or a warning list. A config that allows too little yields quietly shorter output instead of a failure, so an application that wants to know what was dropped has to diff results itself.

Documents need the html element allowlisted and doctype switched on

The two HTML entry points are not interchangeable, and the gap between them is where silent empty output comes from. A document is filtered by the same allowlists as a fragment, but the root element has to survive the filter, so the `<html>` element must appear in the element allowlist or the entire tree is discarded. A doctype does not pass either, unless `allow_doctype` is set to true, which then admits well-formed document type definitions only:

ruby
html = %[
  <!DOCTYPE html>
  <html>
    <b><a href="http://foo.com/">foo</a></b><img src="bar.jpg">
  </html>
]

Sanitize.document(html,
  allow_doctype: true,
  elements: ['html']
)

The documented output is `<!DOCTYPE html><html>foo\n \n</html>`, which misleads in three separate ways. The doctype survives only because the switch was set. The link and the image are gone even though the html element was allowed, because allowlisting a parent element does not allow its children. And the two blank runs between foo and the closing tag are text nodes carried over from the indentation of the input, kept because text is always allowed. A cleaner-looking result than you expected can still mean a config that dropped more than you wanted.

Relaxed mode drops at import and expression while keeping plain rules

`Sanitize::Config::RELAXED` is the widest of the three bundled configs. Images, tables and safe CSS are allowed, links stay limited to FTP, HTTP, HTTPS and mailto protocols, and images are limited to HTTP and HTTPS. The documentation feeds it a stylesheet that packs 3 separate hazards into one string:

ruby
css = %[
  @import url(evil.css);

  a { text-decoration: none; }

  a:hover {
    left: expression(alert('xss!'));
    text-decoration: underline;
  }
]

Sanitize::CSS.stylesheet(css, Sanitize::Config::RELAXED)

All three hazards go. The `@import url(evil.css)` rule is removed, the `left: expression(alert('xss!'))` declaration is removed, and the two harmless declarations stay: `text-decoration: none` on the plain rule and `text-decoration: underline` inside the hover rule. `Sanitize::CSS.properties` narrows a bare declaration list the same way:

ruby
Sanitize::CSS.properties(%[
  left: expression(alert('xss!'));
  text-decoration: underline;
], Sanitize::Config::RELAXED)

Here is a detail the quick start hides. The opening block lists all four entry points side by side with no warning attached, and the warning only arrives further down, attached to standalone CSS: that path is for a standalone CSS file, and CSS destined for a style element or a style attribute has to go through `Sanitize.document()` or `Sanitize.fragment()` with an allowlist, or failure to do so may enable HTML injection attacks. Same constant, same permissive config, different safety depending on which function you call.

MathML and svg are removed by default and the warning appears twice

The one place Sanitize refuses to promise safety is inside foreign elements. `<math>` and `<svg>` are described as foreign elements that do not follow normal HTML parsing rules, so their contents cannot be fully sanitized. The default behaviour is to remove all MathML and SVG elements, and adding either family to a custom element allowlist may create a security vulnerability in the application that does it. The same warning block appears 2 times in the README, once above the link list and once inside usage, which says how load-bearing that boundary is.

Read honestly, this is a closed door rather than a hole in the implementation. There is no configuration that makes Sanitize clean a MathML subtree, so the choice is between stripping it and handing that markup elsewhere. For a product that renders user-supplied formulas or inline icons, that single fact decides whether the gem fits at all. For everyone else the safe default means the risky path has to be requested on purpose, in code, by name.

Three bundled configs differ in images tables and nofollow

Three configs ship ready to use alongside the unnamed default. `Sanitize::Config::RESTRICTED` permits only very simple inline markup, with no links, no images and no block elements, so the sample input returns `<b>foo</b>`. `Sanitize::Config::BASIC` adds formatting elements, links and lists while keeping images and tables out, limits links to FTP, HTTP, HTTPS and mailto, and rewrites every surviving link:

ruby
Sanitize.fragment(html, Sanitize::Config::BASIC)
# => '<b><a href="http://foo.com/" rel="nofollow">foo</a></b>'

That output deserves a pause, because Sanitize is not a pure filter. It adds `rel="nofollow"` to a link that never carried one, to mitigate SEO spam, which is the opposite of what a regex delete pass does to the same string. Anything that compares input against output byte for byte has to expect additions as well as deletions. `Sanitize::Config::RELAXED` then widens the set again to allow images and tables plus safe CSS. Its description in the README stops mid-sentence, right after 'In this mode,', so the widest config is also the one whose final paragraph nobody has written yet.

One gem install, two parsers, and a security policy file

Installation is one command, and nothing else is asked of the application:

bash
gem install sanitize

Beneath that call sit the two parsers rather than a pile of regular expressions, and the safety guarantee follows from the architecture. The documentation states that as long as an allowlist holds only safe markup and CSS, even the most malformed or malicious input is transformed into safe output. Nothing hunts for known-bad strings, so an unknown payload has no pattern to match and no attribute left to keep.

The repository tree shows how the project is laid out and, just as telling, what it treats as its own responsibility: lib holds the code, test holds the suite, sanitize.gemspec and Gemfile handle packaging, Rakefile with .rubocop.yml and .standard.yml drive lint and test tasks, .yardopts configures API docs, CONTRIBUTING.md covers contributions and SECURITY.md gives you somewhere to report a vulnerability. A sanitizer that ships a reporting path and two linter configs is maintained like infrastructure. Sanitize is released under the MIT licence; the newest tag, v7.0.0, dates from 2024-12-30, and the last push to main was 2026-08-23.

Editorial conclusion

Adopt Sanitize when user-supplied HTML or CSS reaches a Ruby app and you want every keep-or-drop decision written into an allowlist instead of a pattern list; MIT licensing makes it easy to embed. Do not adopt it for MathML or SVG, and do not route CSS that will end up inside a page through `Sanitize::CSS.stylesheet`. Before trusting an inherited config, read SECURITY.md, then run `gem install sanitize` and compare output against the exact markup your users paste. The README does not document a rollback path, so pin the version you tested.

Frequently asked questions

What does Sanitize return when I do not pass any configuration?

The default settings are the strictest available, so all HTML markup is stripped and only safe text survives. The documented example turns `<b><a href="http://foo.com/">foo</a></b><img src="bar.jpg">` into the plain string "foo".

How do I allow a style attribute and limit which CSS properties survive?

Allowlist the style attribute in the attributes hash and then allowlist the property names under the css sub-hash. In the documented example, allowlisting only width keeps `width: 1024px` in the style element and `width: 100px` in the attribute, while height and color are removed.

Can Sanitize clean up MathML or SVG content?

No. MathML and SVG are foreign elements that do not follow normal HTML parsing rules, so Sanitize cannot fully sanitize their contents. By default it removes all MathML and SVG elements, and adding them to a custom element allowlist may create a security vulnerability.

Is Sanitize::CSS.stylesheet safe for CSS that will be used inside an HTML page?

The documentation restricts it to a standalone CSS file. CSS for a style element or a style attribute has to be sanitized with `Sanitize.document()` or `Sanitize.fragment()` and an appropriate allowlist, and failure to do so may enable HTML injection attacks.

How do the RESTRICTED, BASIC and RELAXED configs differ?

RESTRICTED allows only very simple inline markup, with no links, images or block elements. BASIC adds formatting elements, links and lists, keeps images and tables out, limits link protocols to FTP, HTTP, HTTPS and mailto, and adds `rel="nofollow"` to every link. RELAXED widens this further to allow images, tables and safe CSS.

Why does sanitizing a full HTML document return almost nothing?

When sanitizing a document, the `<html>` element must be in the element allowlist, or the whole tree is removed. A doctype is dropped as well unless `allow_doctype` is set to true, which then admits well-formed document type definitions.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. rgrove/sanitize on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/rgrove-sanitize.svg)](https://hysenlabs.com/projects/rgrove-sanitize)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/rgrove-sanitize.svg)](https://hysenlabs.com/projects/rgrove-sanitize)

Was this useful?