# Ammonia: a whitelist HTML sanitizer that refuses to add markup

> Ammonia is a Rust crate that cleans untrusted HTML by parsing it with html5ever and rebuilding the document from an allowlist. It is strict about the HTML5 spec, which makes it safe by default but also means it will not turn Markdown into links or pretty-print your text.

**rust-ammonia/ammonia** — Repair and secure untrusted HTML

- Repository: https://github.com/rust-ammonia/ammonia
- Stars: 682 · Forks: 55
- Language: Rust
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/rust-ammonia-ammonia

## A whitelist, not a blocklist, for markup you did not write

Ammonia is a whitelist-based HTML sanitization library, and its stated purpose is specific: preventing cross-site scripting, layout breaking and clickjacking when untrusted user-provided HTML is mixed into a larger web page. The intended audience is therefore not a general text processing need. It is a comment system, a wiki, a content management pipeline, or any service that stores markup from one party and renders it inside a page owned by another.

The whitelist approach is what separates it from the regex-and-blacklist tradition of sanitizers. Nothing is allowed by default, so an element or attribute nobody thought about cannot slip through. The tradeoff is that a whitelist has to be configured for what you intend to permit, and that intent lives in your code rather than in a shared default that the library author chose for you.

The README notes that Ammonia parses with html5ever, the HTML parser from Servo, and serializes document fragments the way browsers do. That is the core design decision, and the reason it resists syntactic obfuscation: a payload that only misbehaves because a naive regex misreads the tag boundary is parsed the same way a browser would parse it before any policy is applied.

## Ammonia parses HTML5 strictly and will not improve your text

The most common surprise with Ammonia is what it refuses to do. The README states that Ammonia parses its input exactly according to the HTML5 specification, which means it will not linkify bare URLs, will not insert line or paragraph breaks, and will not convert (C) into &copy;.

This is a deliberate boundary rather than an omission. A sanitizer that silently rewrites text is doing two jobs, and the second job is where policy decisions get smuggled in. If your input is Markdown and you want Markdown behavior, the documented approach is to run a markup processor first. The README names pulldown-cmark as the example, and the example code shows exactly that order: parse Markdown to HTML first, then clean the resulting HTML.

The other consequence of spec-exact parsing is that entity handling and malformed markup follow the browser model. A tag soup input is parsed into a tree, filtered, and re-serialized, so the output is normalized HTML rather than the input with edits applied. That normalization is a feature for security, and it also means output bytes may differ from input in ways that surprise a test asserting string equality.

## Adding the crate and cleaning your first comment

The dependency is added to your project's Cargo.toml, as the README shows:

```toml
[dependencies]
ammonia = "4.2"
```

Ammonia also declares a minimum Rust version of 1.85 in its own Cargo.toml, and the README carries a badge stating that rustc 1.85.0 or newer is required. The crate is published on crates.io under the name ammonia, and the crate's own documentation lives at docs.rs.

The README then gives a complete example of combining pulldown-cmark with Ammonia for a user-facing comment site:

```rust
use ammonia::clean;
use pulldown_cmark::{Parser, Options, html::push_html};

let text = "[a link](http://www.notriddle.com/)";

let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);

let mut md_parse = Parser::new_ext(text, options);
let mut unsafe_html = String::new();
push_html(&mut unsafe_html, md_parse);

let safe_html = clean(&*unsafe_html);
assert_eq!(safe_html, "<a href=\"http://www.notriddle.com/\">a link</a>");
```

Read the ordering carefully. The Markdown parser produces a string named unsafe_html, and clean is applied to that string, producing safe_html. The name of the intermediate variable is the API's whole message: you are responsible for feeding clean only after your own transformation, and the sanitizer assumes the input is hostile no matter what produced it.

For a service with no Markdown step, the call reduces to passing your stored HTML to clean and writing the returned string into the page.

## Building and traversing a DOM is not the cheapest way to sanitize

Ammonia's own README is honest about its cost model. It builds a DOM, traverses it while replacing unwanted nodes, and serializes it again. The README says it could be faster for what it does, and adds that if you do not want to allow any HTML at all, it is possible to be even faster than that.

That last clause is the useful engineering hint. A large share of user submissions contain no markup worth keeping, and stripping everything is cheaper than parsing and rebuilding. Whether you can take that shortcut depends on your product, but for a comment system that stores plain text it is the obvious route.

The README also compares against a Python baseline, stating that sanitizing an HTML string with bleach-2.0.0 and html5lib-0.999999999 takes about fifteen times longer than with Ammonia 1.0. The benchmark lives in the repository's benchmarks directory, where the Rust side is compiled in release mode and run as the ammonia_bench binary, and the Python side is driven by a separate bleach_bench.py script.

Two caveats apply to those numbers. The comparison is stated against Ammonia 1.0, while the crate is now at 4.2.4, so the figure describes an old version rather than the current one. And the output it reports is a timing on a specific input, the introduction to the Ammonia docs, not a general performance claim. Treat it as evidence that the approach was considered, not as a benchmark to quote in your own evaluation.

## The dependency tree brings in a complete HTML5 parser

Ammonia is not a dependency-light crate, and that is structural rather than accidental. Its Cargo.toml lists html5ever = "0.40" as the parser, maplit = "1.0", url = "2" for URL handling, and cssparser = { version = "0.38.0", default-features = false } for stylesheet parsing.

The categories field lists web-programming and text-processing, and the crate description is simply "HTML Sanitization". The keywords sanitization, html, security and xss point at the same use case.

Two details are worth knowing before you adopt the crate. First, html5ever is a substantial dependency, and if your service already depends on it, the additional footprint is small; if it does not, you are adding an HTML parser to your build to get a sanitizer. Second, the crate enables a configuration check for a custom cfg flag:

```toml
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(ammonia_unstable)'] }
```

That line tells you an unstable feature gate exists behind cfg(ammonia_unstable), and that using it requires declaring the cfg so the compiler does not warn. The crate is built on edition 2021 with rust-version 1.85, and a separate Cargo.lock.msrv file sits at the repository root next to the release process notes and the security policy.

## Bleach and sanitize-html shaped the API you will be using

The README credits two other sanitizer libraries explicitly, saying that most of the API was copied from Bleach for Python and sanitize-html for Node.js. That matters for a reviewer because it tells you where the design conventions come from and what to expect if you have used either of those.

The concrete difference is deployment rather than filtering. Ammonia is a Rust crate that links into your binary, so sanitization happens in-process with no IPC, no sidecar service and no separate runtime to deploy. Bleach is a Python library and sanitize-html is a Node.js package, so in a stack already built on those runtimes, adopting Ammonia means crossing a language boundary or adding a service call. If your application is already Rust, Ammonia is the cheaper shape of the same idea.

The benchmark directory makes that comparison concrete by running both implementations over the same input and timing them separately, one through cargo and one through Python.

On the security record, the README thanks several named researchers, including ChALkeR, whose work on improper markup sanitization helped identify high-level semantic holes in Ammonia, securityMB for an obscure namespace-related injection bug, zzm0902 for another namespace-related injection bug, and xfix for a denial-of-service bug in a recursive destructor. Each of those is linked to a pull request in the repository. Security tooling earns trust through disclosed and fixed bugs, and the list is specific enough to check.

## Two licenses, an explicit policy file, and a 4.x release cadence

Ammonia is offered under a dual license, MIT OR Apache-2.0, and the Cargo.toml records it as license = "MIT OR Apache-2.0". The repository carries both LICENSE-APACHE and LICENSE-MIT files, and the README lists each option with its own link. For a crate embedded in a proprietary product, choosing MIT keeps the notice obligation small, while Apache-2.0 adds an explicit patent grant. That is a decision for your legal review, but the choice is available without forking or patching.

The repository also has the files a security conscious project tends to have: SECURITY.md for reporting, CHANGELOG.md for release history, RELEASE_PROCESS.md for how releases are made, CODE_OF_CONDUCT.md, and bors.toml for the merge automation used during development. The README points to CHANGELOG.md rather than duplicating a changelog, which means the version history lives in the repository and moves with the code.

The release list shows three tags in the second half of October 2026: v4.2.3 on 2026-10-11, then v4.1.8 and v4.2.4 both on 2026-10-17. Two branches moving on the same day means the project maintains more than one line, and a parallel 4.1 series is getting fixes alongside 4.2. The last push was on 2026-10-17. The crate version in Cargo.toml is 4.2.4, matching the newest tag, and the README's dependency snippet pins the minor series with ammonia = "4.2".

## Conclusion

Ammonia suits Rust services that must embed user supplied HTML into their own pages, especially when the surrounding stack already uses html5ever. It does not suit a pipeline that expects Markdown conveniences, because it will not linkify bare URLs or insert line breaks, so a markup processor such as pulldown-cmark has to run first. Before adopting it, check the allowlist in your build against the tags your product actually needs, confirm your Rust toolchain meets the 1.85 floor in Cargo.toml, and read CHANGELOG.md for the 4.x line changes that touch your element set.

## FAQ

### How do I add the Ammonia crate to a Rust project?

Add it to your Cargo.toml as the README shows, with ammonia = "4.2" under [dependencies]. The crate requires Rust 1.85 or newer, and its documentation is published at docs.rs.

### Does Ammonia convert Markdown or linkify plain URLs?

No. Ammonia parses its input exactly according to the HTML5 specification, so it will not linkify bare URLs, insert line breaks, or convert (C) into &copy;. The README recommends running a markup processor such as pulldown-cmark before the sanitizer.

### What license is Ammonia released under?

It is dual licensed under MIT OR Apache-2.0, with both LICENSE-MIT and LICENSE-APACHE files in the repository. The Cargo.toml records the same choice.

### How does Ammonia compare with Bleach and sanitize-html?

The README says the Ammonia API was copied mostly from Bleach for Python and sanitize-html for Node.js. It also states that sanitizing with bleach-2.0.0 and html5lib-0.999999999 takes about fifteen times longer than with Ammonia 1.0, measured on the Ammonia documentation introduction.

## Sources

- [Issues](https://github.com/rust-ammonia/ammonia/issues)
- [License: Apache-2.0](https://github.com/rust-ammonia/ammonia/blob/master/LICENSE)
- [README](https://github.com/rust-ammonia/ammonia/blob/master/README.md)
- [Releases](https://github.com/rust-ammonia/ammonia/releases)
- [rust-ammonia/ammonia on GitHub](https://github.com/rust-ammonia/ammonia)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/rust-ammonia-ammonia
