# HtmlSanitizer: the .NET HTML allowlist that also blocks javascript: URLs in style attributes

> HtmlSanitizer is a .NET library that cleans HTML fragments and documents of constructs leading to XSS, using AngleSharp to parse rather than pattern matching, which also shields callers from tag poisoning. Configuration is layered through allowlists for tags, attributes, CSS properties, at-rules and URI schemes, and its sharpest edge is that UriAttributes is a screening list rather than an allowlist.

**mganss/HtmlSanitizer** — Cleans HTML to avoid XSS attacks

- Repository: https://github.com/mganss/HtmlSanitizer
- Stars: 1,710 · Forks: 229
- Language: C#
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/mganss-htmlsanitizer

## What it is: a parser-based filter, not a regular expression

HtmlSanitizer is a .NET library for cleaning HTML fragments and documents from constructs that can lead to XSS attacks, and it uses AngleSharp to parse, manipulate and render HTML and CSS. That dependency is the whole design. A regular expression filter has to guess where a tag begins and ends, and an attacker gets to choose the input, so the guessing is exactly the attack surface. A real parser knows the tree, so it knows which characters are inside an attribute value and which are text. The README makes a second point that follows from the same decision, that because HtmlSanitizer is based on a robust HTML parser it can also shield you from deliberate or accidental tag poisoning, where invalid HTML in one fragment can corrupt the whole document leading to broken layout or style. The primary language is C#, the licence is MIT, and the library targets netstandard2.0, net46 and net8.0.

## The example, and what it actually proves

The README's example is worth reading as a demonstration of three separate behaviours rather than one. The input HTML contains a script tag with an alert, a div with an onload attribute, an img with a src of test.png, and a style attribute carrying a background-image with a url(javascript:alert('xss')) and a margin. The output keeps the div and its background-color, keeps the img but rewrites its src to an absolute URL, https://www.example.com/test.png, and keeps only the margin from the style, dropping the javascript: background-image. So the library does three things at once, stripping disallowed tags, stripping disallowed attributes, and rewriting relative URIs against the base URI you passed as the second argument to Sanitize. The javascript: URI inside the style attribute is not merely filtered as a URI, it is removed by the CSS property allowlist, since background-image is not an allowed CSS property by default. There is an online demo and a .NET Fiddle link if you want to try inputs without setting up a project.

```C#
using Ganss.Xss;
var sanitizer = new HtmlSanitizer();
var html = @"<script>alert('xss')</script><div onload=""alert('xss')"""
    + @"style=""background-color: rgba(0, 0, 0, 1)"">Test<img src=""test.png"""
    + @"style=""background-image: url(javascript:alert('xss')); margin: 10px"""></div>";
var sanitized = sanitizer.Sanitize(html, "https://www.example.com");
```

## The allowlists, which are the entire configuration surface

The customization list is long and each entry is a single concept. AllowedTags configures allowed HTML tags and all other tags are stripped. AllowedAttributes does the same for attributes. AllowedCssProperties does it for CSS property names. AllowedAtRules does it for CSS at-rules. AllowedSchemes configures allowed URI schemes and all other URIs are stripped. UriAttributes names the HTML attributes that contain a URI, such as src and href, and UriListAttributes names those that contain a list of URIs, such as srcset and ping, where every entry is checked separately. A base URI can be provided to resolve relative URIs against. SanitizeFragment() sanitizes for the element the fragment will be inserted into rather than for body, which keeps markup such as a lone th that is only valid deeper in the tree. And cancelable events are raised before a tag, attribute or style is removed, so a caller can log or override a removal.

## UriAttributes is a screening list, and that is the sharpest edge here

The README spends a paragraph on a distinction that will bite anyone building an allowlist from scratch, and it is the most valuable thing in the documentation. UriAttributes is a screening list rather than an allow list, so an attribute that is not in it is not treated as carrying a URI at all, and its value is never checked against AllowedSchemes. The consequence is that it is easy to allow href without also adding it to UriAttributes, and then a javascript: href survives untouched. The example makes the point explicitly, with an options object built from HtmlSanitizerOptions.Empty(), a and href allowed, and the comment that UriAttributes is empty so href is not screened against AllowedSchemes, and that an anchor with a javascript: URI is preserved as-is. This is a footgun that a library with a different design would have removed, and the mitigation is documentation, which is why the README leads with it.

```C#
var options = HtmlSanitizerOptions.Empty();
options.AllowedTags.Add("a");
options.AllowedAttributes.Add("href");
var sanitizer = new HtmlSanitizer(options);
// UriAttributes is empty, so href is *not* screened against AllowedSchemes
// <a href="javascript:alert(1)">click</a> is preserved as-is
```

## UriAttributes and AllowedSchemes have to be set together

The second half of the same trap is documented immediately after. UriAttributes and AllowedSchemes need to be set together, because screening an attribute against an empty scheme set drops every absolute URI, valid https ones included. The example is a sanitizer built from Empty() with a and href allowed and href added to UriAttributes, and the comment that AllowedSchemes left empty means every absolute URI is dropped, so an anchor to https://example.com becomes a bare anchor with the text and no link. So there are two failure modes and they are opposites, leave UriAttributes out and the unsafe scheme gets through, leave AllowedSchemes out and the safe ones get dropped. A caller building a configuration from scratch has to add both, and the defaults that new HtmlSanitizer() supplies already have them consistent.

```C#
var options = HtmlSanitizerOptions.Empty();
options.AllowedTags.Add("a");
options.AllowedAttributes.Add("href");
options.UriAttributes.Add("href");
// AllowedSchemes left empty - every absolute URI is dropped
var sanitizer = new HtmlSanitizer(options);
// <a href="https://example.com">ok</a> becomes <a>ok</a>
```

## HtmlSanitizerOptions: pre-populated by default, or a blank slate on request

The configuration can be done in one go by passing an HtmlSanitizerOptions object to the constructor, and there are three distinct starting points. A plain new HtmlSanitizerOptions() starts pre-populated with the same defaults as new HtmlSanitizer(), so you only need to set what you want to change. You can clear a collection and add to it, and the example does exactly that with AllowedTags, adding a and img, with a comment that AllowedAttributes, UriAttributes, AllowedSchemes and the rest remain at their defaults. Or you can reassign a collection outright, for example AllowedTags as a HashSet of a and img and AllowedAttributes as a HashSet of href and src, and the README notes this still replaces the default for that property alone while the others are unaffected. And if you want a blank slate, nothing allowed until you add it, you start from HtmlSanitizerOptions.Empty().

```C#
var options = new HtmlSanitizerOptions();
options.AllowedTags.Clear();
options.AllowedTags.Add("a");
options.AllowedTags.Add("img");
// AllowedAttributes, UriAttributes, AllowedSchemes, etc. remain at their defaults
var sanitizer = new HtmlSanitizer(options);
```

## The default tag list, and what a big list implies

The README publishes the tags allowed by default in full, and it is a long list, which is itself a design statement. It runs from a, abbr, acronym, address, area, article, aside, b, bdi, big, blockquote, body, br, button, caption, center, cite, code, col, colgroup, data, datalist, dd, del, details, dfn, dir, div, dl, dt, em, fieldset, figcaption, figure, font, footer, form, h1 through h6, head, header, hr, html, i, img, input, ins, kbd, keygen and onward. A permissive default is a deliberate choice, because a library that strips too much breaks rich text, and a library that assumes you will narrow it is one where a caller who does not read the documentation ships the permissive default. The tag list is only half the picture, since the attribute and CSS allowlists do more of the work in practice, and it is the CSS allowlist that removed the javascript: background-image in the example above.

## Upkeep, targets and where the project stands

The last push to the repository was on 2026-10-16, the repository is not archived, and the licence is MIT with the text in LICENSE.md. The releases show two lines running in parallel, v9.2.995 on 2026-08-10, v9.2.1039 on 2026-08-28, and a v10.0.1037-beta on 2026-08-27, so a major version is in beta while the 9.x line continues to receive patches. That pattern is the one to plan around, because a 9.x consumer gets fixes and a 10.x consumer gets a preview. The build badges cover netstandard2.0, net46 and net8.0, and the CI configuration is AppVeyor for the build, with codecov and a SonarCloud quality gate badge alongside it. The repository layout is small, with an HtmlSanitizer.sln, a src/ directory, a test/ directory and an appveyor.yml, and the wiki carries more example code and a description of the possible options.

## Conclusion

HtmlSanitizer suits a .NET application that accepts HTML from users, from a CMS field, from an email, from a pasted document or from a rich text editor, and that needs the result to be safe to render without a full browser sandbox. It is the wrong choice if you can constrain the input to plain text or to a strict subset you generate yourself, since a sanitizer is a filter over untrusted input rather than a template, and a poor fit if you are working in a runtime that does not support the .NET Standard 2.0, .NET Framework 4.6.2 or .NET 8.0 targets it ships. Before adopting it, read the section on UriAttributes and AllowedSchemes, because the two must be configured together and building an allowlist from HtmlSanitizerOptions.Empty() without them will either leave href unscreened or drop every absolute URI including valid https ones.

## FAQ

### What does HtmlSanitizer do?

It is a .NET library for cleaning HTML fragments and documents from constructs that can lead to XSS attacks. It uses AngleSharp to parse, manipulate and render HTML and CSS, which also shields you from tag poisoning where invalid HTML in one fragment corrupts the whole document.

### How do I configure HtmlSanitizer?

You configure allowed tags, attributes, CSS properties, CSS at-rules and URI schemes, and name the attributes that carry URIs through UriAttributes and UriListAttributes. All of it can be passed at once in an HtmlSanitizerOptions object, which new HtmlSanitizerOptions() pre-populates with the same defaults as new HtmlSanitizer(), while HtmlSanitizerOptions.Empty() gives you a blank slate.

### What is the UriAttributes gotcha in HtmlSanitizer?

UriAttributes is a screening list rather than an allow list, so an attribute not in it is not treated as carrying a URI at all and its value is never checked against AllowedSchemes. Building an allowlist from Empty() and allowing href without adding it to UriAttributes leaves a javascript: href untouched, and the two lists must be set together, since screening an attribute against an empty scheme set drops every absolute URI including valid https ones.

### How do I sanitize a fragment for a specific element?

Use SanitizeFragment() rather than Sanitize(), which sanitizes for the element the fragment will be inserted into instead of for body. That keeps markup such as a lone th that is only valid deeper in the tree, and you can also provide a base URI that relative URIs are resolved against.

### Which .NET versions does HtmlSanitizer support?

The package targets netstandard2.0, net46 and net8.0. The releases include v9.2.1039 from 2026-08-28 alongside a v10.0.1037-beta from 2026-08-27, so a major version is in beta while the 9.x line continues to be patched.

## Sources

- [Issues](https://github.com/mganss/HtmlSanitizer/issues)
- [License: MIT](https://github.com/mganss/HtmlSanitizer/blob/master/LICENSE)
- [mganss/HtmlSanitizer on GitHub](https://github.com/mganss/HtmlSanitizer)
- [README](https://github.com/mganss/HtmlSanitizer/blob/master/README.md)
- [Releases](https://github.com/mganss/HtmlSanitizer/releases)

---

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