Sanitizer
The browser's own XSS filter as a thing of its own: sanitize markup to a string, or configure exactly which elements a comment field allows and reuse that configuration for every fragment.
@inject Bit.Butil.Sanitizer sanitizerMDN reference
SetHtml on an element already
sanitizes; this is the same filter, configurable, reusable, and able to hand back a string.
Sanitize returns null rather than the markup untouched, so an
unsupported browser can never be handed the input as if it had been cleaned. Check
IsSetHtmlSupported and have a server-side path for the rest.
Two different questions. IsSetHtmlSupported asks for the sanitizing sink, which is all that sanitizing with the default configuration needs. IsSupported asks for the configurable Sanitizer object, which is what Create needs - a browser can have the first without the second.
@inject Bit.Butil.Sanitizer sanitizer
var canSanitize = await sanitizer.IsSetHtmlSupported();
var canConfigure = await sanitizer.IsSupported();Parse untrusted markup, drop what isn't safe, and get the result back as text - before storing it, indexing it or sending it on. The markup is parsed into a detached element that is never in the document, so nothing in it loads, runs or is announced on the way through.
var clean = await sanitizer.Sanitize(userHtml);
if (clean is null)
{
// no sanitizing sink in this browser - fall back to the server
}A configuration is either an allow-list or a deny-list, not both: setting Elements means 'only these', setting RemoveElements means 'the baseline, minus these'. Building a sanitizer is the expensive half of sanitizing one short fragment, so build it once per policy - 'what a comment may contain' - and reuse the handle for every comment.
await using var comments = await sanitizer.Create(new SanitizerConfig
{
Elements = ["p", "b", "i", "em", "strong", "a", "ul", "ol", "li"],
Attributes = ["href", "title"],
Comments = false,
});
// Null when the browser has no configurable Sanitizer - the unconfigured sink is still there:
var clean = comments is null
? await sanitizer.Sanitize(userHtml)
: await comments.Sanitize(userHtml);The same filter writing into an element rather than returning a string - what SetHtml does, with a configuration when you pass a handle. The result below is rendered live: the script and the event handler in the hostile sample are gone from it.
Bit.Butil.Sanitizer sanitizer
<div @ref="preview"></div>
{
private ElementReference preview;
private SanitizerHandle? commentsHandle; // from sanitizer.Create
// The markup never comes back to C# in between, so there is no window in which the raw string
// could be written somewhere else by accident.
private async Task Show(string userHtml)
{
await sanitizer.SanitizeInto(preview, userHtml);
// or under a configuration:
await commentsHandle!.SanitizeInto(preview, userHtml);
}
}API reference
ValueTask<bool> IsSupported()ValueTask<bool> IsSetHtmlSupported()ValueTask<string?> Sanitize(string html)ValueTask<bool> SanitizeInto(ElementReference element, string html)ValueTask<SanitizerHandle?> Create(SanitizerConfig config)ValueTask<JsonElement?> GetDefaultConfig()ValueTask DisposeAsync()ValueTask<string?> Sanitize(string html)ValueTask<bool> SanitizeInto(ElementReference element, string html)ValueTask<JsonElement?> GetConfig()ValueTask DisposeAsync()Elements | RemoveElements | ReplaceWithChildrenElements | Attributes | RemoveAttributes | Comments | DataAttributes