Skip to content

TrustedTypes: Add boilerplate for the xxxHtmlUnsafe() methods #40420

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

hamishwillee
Copy link
Collaborator

@hamishwillee hamishwillee commented Jul 18, 2025

This updates the following methods to explain how they are used with TrustedTypes:

Initially I have just subedited a little and added the (not-quite-right) boilerplate from #37952. This is mostly to create a environment for working out where to go moving forward.

Part of #37518 (tracking issue)


These methods are tricky because the standard boilerplate recommending using trusted HTML is not true, or at least not reasonable to follow:

Warning

This API parses its input as HTML, writing the result into the DOM.
APIs like this are known as injection sinks, and are potentially a vector for cross-site-scripting (XSS) attacks, if the input originally came from an attacker.

For this reason it's much safer to pass only {{domxref("TrustedHTML")}} objects into this method, and to enforce this using the require-trusted-types-for CSP directive.
This means you can be sure that the input has been passed through a transformation function, which has the chance to sanitize the input to remove potentially dangerous markup, such as {{htmlelement("script")}} elements and event handler attributes.

The reason is that these methods take a sanitizer argument that can inject the input string in a context-aware way. You could use the transform but you would be better off using this sanitizer, and you wouldn't do both. So why do we need TT in this case, and what would it look like.

The simplistic answer is probably that if we're enforcing TTs then we'll need to pass at TT or have a default TT or this will fail. So for this case the simple answer would be "you don't need to do anything special but you should create an unsafeSanitizedHTML policy and use that for all these APIs. In other words it isn't useful.

Is there a way to make it useful. I played with prompting ChatGPT to generate code that achieves the goal of TT, which is to ensure that all code is audited, and that auditing is in a known place. It didn't really use TT for this, but it did come up with the idea of safeParseHTML() which wraps the method with something that ensures you always pass the required policy, and ensures that your sanitizer is always one that is "registered/trusted".

const approvedSanitizers = new WeakSet();

function registerSanitizer(fn) {
  approvedSanitizers.add(fn);
  return fn;
}

const parsePolicy = trustedTypes.createPolicy('parseHTMLPolicy', {
  createHTML(input) {
    // Developer must pass input through here manually
    return input;
  }
});

function safeParseHTML(rawInput, sanitizer) {
  if (!approvedSanitizers.has(sanitizer)) {
    throw new TypeError('Unapproved sanitizer passed to safeParseHTML');
  }

  const trusted = parsePolicy.createHTML(rawInput);
  return parseHTMLUnsafe(trusted, sanitizer);
}
const mySanitizer = registerSanitizer(
  new HTMLSanitizer({ allowElements: ['b', 'i', 'a'] })
);

safeParseHTML('<b>hello</b>', mySanitizer);

@wbamberg @lukewarlow Would love your thoughts on how this API should be used with trusted types.

@github-actions github-actions bot added Content:WebAPI Web API docs size/s [PR only] 6-50 LoC changed labels Jul 18, 2025
Copy link
Contributor

github-actions bot commented Jul 18, 2025

Comment on lines 11 to 16
> [!WARNING]
> This API parses its input as HTML, writing the result into the DOM.
> APIs like this are known as [injection sinks](/en-US/docs/Web/API/Trusted_Types_API#concepts_and_usage), and are potentially a vector for [cross-site-scripting (XSS)](/en-US/docs/Web/Security/Attacks/XSS) attacks, if the input originally came from an attacker.
>
> For this reason it's much safer to pass only {{domxref("TrustedHTML")}} objects into this method, and to [enforce](/en-US/docs/Web/API/Trusted_Types_API#using_a_csp_to_enforce_trusted_types) this using the [`require-trusted-types-for`](/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for) CSP directive.
> This means you can be sure that the input has been passed through a transformation function, which has the chance to [sanitize](/en-US/docs/Web/Security/Attacks/XSS#sanitization) the input to remove potentially dangerous markup, such as {{htmlelement("script")}} elements and event handler attributes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking maybe for (all) these cases we modify the boilerplate like this.

Suggested change
> [!WARNING]
> This API parses its input as HTML, writing the result into the DOM.
> APIs like this are known as [injection sinks](/en-US/docs/Web/API/Trusted_Types_API#concepts_and_usage), and are potentially a vector for [cross-site-scripting (XSS)](/en-US/docs/Web/Security/Attacks/XSS) attacks, if the input originally came from an attacker.
>
> For this reason it's much safer to pass only {{domxref("TrustedHTML")}} objects into this method, and to [enforce](/en-US/docs/Web/API/Trusted_Types_API#using_a_csp_to_enforce_trusted_types) this using the [`require-trusted-types-for`](/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for) CSP directive.
> This means you can be sure that the input has been passed through a transformation function, which has the chance to [sanitize](/en-US/docs/Web/Security/Attacks/XSS#sanitization) the input to remove potentially dangerous markup, such as {{htmlelement("script")}} elements and event handler attributes.
> [!WARNING]
> This API parses its input as HTML, writing the result into the DOM.
> APIs like this are known as [injection sinks](/en-US/docs/Web/API/Trusted_Types_API#concepts_and_usage), and are potentially a vector for [cross-site-scripting (XSS)](/en-US/docs/Web/Security/Attacks/XSS) attacks, if the input originally came from an attacker.
> It is better to use XSS-safe methods where possible, such as {{domxref("ShadowRoot.setHTML()")}}.
>
> You should ensure that this method is called with as restrictive a sanitizer as possible.
> If [Trusted Types are enforced](/en-US/docs/Web/API/Trusted_Types_API#using_a_csp_to_enforce_trusted_types), you will need pass {{domxref("TrustedHTML")}} objects into this method.
> The policy for creating these types should be a pass-through, as the inputs do not need to be sanitized twice.

@@ -22,12 +29,12 @@ setHTMLUnsafe(input, options)
### Parameters

- `input`
- : A string or {{domxref("TrustedHTML")}} instance defining HTML to be parsed.
- : A {{domxref("TrustedHTML")}} or string instance defining HTML to be parsed.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intent is that for all injection sinks we will put the trusted type as the first option.

@github-actions github-actions bot added size/m [PR only] 51-500 LoC changed and removed size/s [PR only] 6-50 LoC changed labels Jul 18, 2025
@@ -227,6 +235,31 @@ With this approach you can create safe HTML, but you aren't forced to.

{{EmbedLiveSample("setHTMLUnsafe() live example","100","350px")}}

<!--
Copy link
Collaborator Author

@hamishwillee hamishwillee Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a (hidden) example of what we might show for the htmlunsafe examples.

The point being that you need to do this to avoid the methods throwing exceptions.
What we need to show here is a very restrictive sanitiser that still allows something that is XSS unsafe.

  • Obvious thing is a script element, but is there anything else that might be better for an example? It is "the most unsafe" thing, so seems unsubtle to allow it.
    • Will modify to use default sanitizer then add an allowed "bad" item, so that we're demonstrating "disallow what you can".

Will also note that if you're using the default sanitizer there is no reason not to use the safe method setHTML.

Note, this is a "call for help". Is it reasonable? Should we be recommending something else? Who can we ask?

@@ -8,6 +8,13 @@ browser-compat: api.Document.parseHTMLUnsafe_static

{{APIRef("DOM")}}

> [!WARNING]
> This API parses its input as HTML, writing the result into the DOM.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
> This API parses its input as HTML, writing the result into the DOM.
> This method parses its input as HTML, writing the result into the DOM.

Comment on lines +15 to +16
> For this reason it's much safer to pass only {{domxref("TrustedHTML")}} objects into this method, and to [enforce](/en-US/docs/Web/API/Trusted_Types_API#using_a_csp_to_enforce_trusted_types) this using the [`require-trusted-types-for`](/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for) CSP directive.
> This means you can be sure that the input has been passed through a transformation function, which has the chance to [sanitize](/en-US/docs/Web/Security/Attacks/XSS#sanitization) the input to remove potentially dangerous markup, such as {{htmlelement("script")}} elements and event handler attributes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
> For this reason it's much safer to pass only {{domxref("TrustedHTML")}} objects into this method, and to [enforce](/en-US/docs/Web/API/Trusted_Types_API#using_a_csp_to_enforce_trusted_types) this using the [`require-trusted-types-for`](/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for) CSP directive.
> This means you can be sure that the input has been passed through a transformation function, which has the chance to [sanitize](/en-US/docs/Web/Security/Attacks/XSS#sanitization) the input to remove potentially dangerous markup, such as {{htmlelement("script")}} elements and event handler attributes.
> For this reason it's much safer to pass only {{domxref("TrustedHTML")}} objects into this method, and to [enforce](/en-US/docs/Web/API/Trusted_Types_API#using_a_csp_to_enforce_trusted_types) this using the [`require-trusted-types-for`](/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for) CSP directive.
> You can reduce the risk by passing {{domxref("TrustedHTML")}} objects into this method instead of string, and [enforcing trusted types](/en-US/docs/Web/API/Trusted_Types_API#using_a_csp_to_enforce_trusted_types) using the [`require-trusted-types-for`](/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for) CSP directive.
> This ensures that the input is passed through a transformation function, which has the chance to [sanitize](/en-US/docs/Web/Security/Attacks/XSS#sanitization) the input to remove potentially dangerous markup, such as {{htmlelement("script")}} elements and event handler attributes.

@lukewarlow
Copy link
Contributor

So one thing to keep in mind is that right now no browser ships the sanitizer and even once they do on older browser releases all these functions will very much be XSS sinks. So for that reason I think MDN should be consistent with it's guidance, if the approach is to recommend using trusted types then it should be done here.

Regarding the case of once you have the sanitizer API and you use it, it does get interesting.

While the sanitizer API is better designed (avoids mXSS issues) it isn't enforcing anything in particular (of course on the safe variants it will). Trusted types allows you to enforce that it goes through an approved policy, which you control the substance of.

This is a tricky case where you don't necessarily want to provide a no-op policy because your sanitizer config might be wrong or too lax for your liking. But equally you probably don't want to run it through two sanitizers.

I think the general advice still applies though. They should be passing a TrustedHTML argument. It's very context dependent as to what the policy should look like though.

I'll raise an issue on the sanitizer API repo to discuss guidance on this point.

@hamishwillee
Copy link
Collaborator Author

@lukewarlow You make some good points. I've been imagining the release in browsers of sanitizer as imminent, but of course, there is no guarantee that this is true.

I'll raise an issue on the sanitizer API repo to discuss guidance on this point.

Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Content:WebAPI Web API docs size/m [PR only] 51-500 LoC changed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants