ShadowRoot: setHTMLUnsafe() method
Limited availability
This feature is not Baseline because it does not work in some of the most widely-used browsers.
Warning: This method 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.
You can mitigate this risk by always passing TrustedHTML
objects instead of strings and enforcing trusted types.
See Security considerations for more information.
Note:
ShadowRoot.setHTML()
should almost always be used instead of this method — on browsers where it is supported — as it always removes XSS-unsafe HTML entities.
The setHTMLUnsafe()
method of the ShadowRoot
interface can be used to parse HTML input into a DocumentFragment
, optionally filtering out unwanted elements and attributes, and then use it to replace the existing tree in the Shadow DOM.
Syntax
setHTMLUnsafe(input)
setHTMLUnsafe(input, options)
Parameters
input
-
A
TrustedHTML
or string instance defining HTML to be parsed. options
Optional-
An options object with the following optional parameters:
sanitizer
Optional-
A
Sanitizer
orSanitizerConfig
object that defines what elements of the input will be allowed or removed. This can also be a string with the value"default"
, which applies aSanitizer
with the default (XSS-safe) configuration. If not specified, no sanitizer is used.Note that generally a
Sanitizer
is expected to be more efficient than aSanitizerConfig
if the configuration is to reused.
Return value
None (undefined
).
Exceptions
TypeError
-
This is thrown if:
input
is passed a string when Trusted Types are enforced by a CSP and no default policy is defined.options.sanitizer
is passed a:- value that is not a
Sanitizer
,SanitizerConfig
, or string. - non-normalized
SanitizerConfig
(one that includes both "allowed" and "removed" configuration settings). - string that does not have the value
"default"
.
- value that is not a
Description
The setHTMLUnsafe()
method can be used to parse a string of HTML, optionally filtering out unwanted elements and attributes, and use it to replace the existing Shadow DOM.
Unlike with ShadowRoot.innerHTML
, declarative shadow roots in the input will be parsed into the DOM.
If the string of HTML defines more than one declarative shadow root in a particular shadow host then only the first ShadowRoot
is created — subsequent declarations are parsed as <template>
elements within that shadow root.
setHTMLUnsafe()
doesn't perform any sanitization by default.
If no sanitizer is passed as a parameter, all HTML entities in the input will be injected.
Security considerations
The suffix "Unsafe" in the method name indicates that it does not enforce removal of all XSS-unsafe HTML entities (unlike ShadowRoot.setHTML()
).
While it can do so if used with an appropriate sanitizer, it doesn't have to use an effective sanitizer, or any sanitizer at all!
The method is therefore a possible vector for Cross-site-scripting (XSS) attacks, where potentially unsafe strings provided by a user are injected into the DOM without first being sanitized.
You should mitigate this risk by always passing TrustedHTML
objects instead of strings, and enforcing trusted types using the require-trusted-types-for
CSP directive.
This ensures that the input is passed through a transformation function, which has the chance to sanitize the input to remove potentially dangerous markup (such as <script>
elements and event handler attributes), before it is injected.
Using TrustedHTML
makes it possible to audit and check that sanitization code is effective in just a few places, rather than scattered across all your injection sinks.
You should not have to pass a sanitizer to the method when using TrustedHTML
.
If for any reason you can't use TrustedHTML
(or even better, setHTML()
) then the next safest option is to use setHTMLUnsafe()
with the XSS-safe default Sanitizer
.
When should setHTMLUnsafe()
be used?
setHTMLUnsafe()
should almost never be used if ShadowRoot.setHTML()
is available, because there are very few (if any) cases where user-provided HTML input should need to include XSS-unsafe elements.
Not only is setHTML()
safe, but it avoids having to consider trusted types.
Using setHTMLUnsafe()
might be appropriate if:
-
You can't use
setHTML()
or trusted types (for whatever reason) and you want to have the safest possible filtering. In this case you might usesetHTMLUnsafe()
with the defaultSanitizer
to filter all XSS-unsafe elements. -
You can't use
setHTML()
and the input might contain declarative shadow roots, so you can't useShadowRoot.innerHTML
. -
You have an edge case where you have to allow HTML input that includes a known set of unsafe HTML entities.
You can't use
setHTML()
in this case, because it strips all unsafe entities. You could usesetHTMLUnsafe()
without a sanitizer orinnerHTML
, but that would allow all unsafe entities.A better option here is to call
setHTMLUnsafe()
with a sanitizer that allows just those dangerous elements and attributes we actually need. While this is still unsafe, it is safer than allowing all of them.
For the last point, consider a situation where your code relies on being able to use unsafe onclick
handlers.
The following code shows the effect of the different methods and sanitizers on this case.
const shadow = document.querySelector("#host").shadowRoot;
const input = "<img src=x onclick=alert('onclick') onerror=alert('onerror')>";
// Safe - removes all XSS-unsafe entities.
shadow.setHTML(input);
// Removes no event handler attributes
shadow.setHTMLUnsafe(input);
shadow.innerHTML = input;
// Safe - removes all XSS-unsafe entities.
const configSafe = new Sanitizer();
shadow.setHTMLUnsafe(input, { sanitizer: configSafe });
// Removes all XSS-unsafe entities except `onclick`
const configLessSafe = new Sanitizer();
config.allowAttribute("onclick");
shadow.setHTMLUnsafe(input, { sanitizer: configLessSafe });
Examples
>setHTMLUnsafe() with Trusted Types
To mitigate the risk of XSS, we'll first create a TrustedHTML
object from the string containing the HTML, and then pass that object to setHTMLUnsafe()
.
Since trusted types are not yet supported on all browsers, we define the trusted types tinyfill.
This acts as a transparent replacement for the trusted types JavaScript API:
if (typeof trustedTypes === "undefined")
trustedTypes = { createPolicy: (n, rules) => rules };
Next we create a TrustedTypePolicy
that defines a createHTML()
for transforming an input string into TrustedHTML
instances.
Commonly implementations of createHTML()
use a library such as DOMPurify to sanitize the input as shown below:
const policy = trustedTypes.createPolicy("my-policy", {
createHTML: (input) => DOMPurify.sanitize(input),
});
Then we use this policy
object to create a TrustedHTML
object from the potentially unsafe input string:
// The potentially malicious string
const untrustedString = "abc <script>alert(1)<" + "/script> def";
// Create a TrustedHTML instance using the policy
const trustedHTML = policy.createHTML(untrustedString);
Now that we have trustedHTML
, the code below shows how you can use it with setHTMLUnsafe()
.
First we create the ShadowRoot
we want to target.
This could be created programmatically using Element.attachShadow()
but for this example we'll create the root declaratively.
<div id="host">
<template shadowrootmode="open">
<span>A span element in the shadow DOM</span>
</template>
</div>
We then get a handle to the shadow root from the #host
element and call setHTMLUnsafe()
.
The input has been through the transformation function, so we don't pass a sanitizer to the method.
const shadow = document.querySelector("#host").shadowRoot;
// setHTMLUnsafe() with no sanitizer (no filtering)
shadow.setHTMLUnsafe(trustedHTML);
Using setHTMLUnsafe() without Trusted Types
This example demonstrates the case where we aren't using trusted types, so we'll be passing sanitizer arguments.
The code first creates an untrusted string and shows a number of ways a sanitizer can be passed to the method.
// The potentially malicious string
const untrustedString = "abc <script>alert(1)<" + "/script> def";
// Get the shadow root element
const shadow = document.querySelector("#host").shadowRoot;
// Define custom Sanitizer and use in setHTMLUnsafe()
// This allows only elements: div, p, button, script
const sanitizer1 = new Sanitizer({
elements: ["div", "p", "button", "script"],
});
shadow.setHTMLUnsafe(untrustedString, { sanitizer: sanitizer1 });
// Define custom SanitizerConfig within setHTMLUnsafe()
// Removes the <script> element but allows other potentially unsafe entities.
shadow.setHTMLUnsafe(untrustedString, {
sanitizer: { removeElements: ["script"] },
});
setHTMLUnsafe()
live example
This example provides a "live" demonstration of the method when called with different sanitizers.
The code defines buttons that you can click to inject a string of HTML.
One button injects the HTML without sanitizing it at all, and the second uses a custom sanitizer that allows <script>
elements but not other unsafe items.
The original string and injected HTML are logged so you can inspect the results in each case.
Note: Because we want to show how the sanitizer argument is used, the following code injects a string rather than a trusted type. You should not do this in production code.
HTML
The HTML defines two <button>
elements for injecting the HTML with no sanitizer and with a custom sanitizer (respectively), another button to reset the example, and a <div>
that contains the declarative shadow root.
<button id="buttonNoSanitizer" type="button">None</button>
<button id="buttonAllowScript" type="button">allowScript</button>
<button id="reload" type="button">Reload</button>
<div id="host">
<template shadowrootmode="open">
<span>I am in the shadow DOM </span>
</template>
</div>
JavaScript
First we define the handler for the reload button.
const reload = document.querySelector("#reload");
reload.addEventListener("click", () => document.location.reload());
Then we define the input string to inject into the shadow root, which will be the same for all cases.
This contains the <script>
element and the onclick
handler, both of which are considered XSS-unsafe.
We also get variable shadow
, which is our handle to the shadow root.
// Define unsafe string of HTML
const unsanitizedString = `
<div>
<p>Paragraph to inject into shadow DOM. <button onclick="alert('You clicked the button!')">Click me</button></p>
<script src="path/to/a/module.js" type="module"><script>
</div>
`;
const shadow = document.querySelector("#host").shadowRoot;
Next we define the click handler for the button that sets the shadow root using setHTMLUnsafe()
without passing a sanitizer.
As there is no sanitizer, we expect the injected HTML to match the input string.
const buttonNoSanitizer = document.querySelector("#buttonNoSanitizer");
buttonNoSanitizer.addEventListener("click", () => {
// Set the content of the element with no sanitizer
shadow.setHTMLUnsafe(unsanitizedString);
// Log HTML before sanitization and after being injected
logElement.textContent = "No sanitizer\n\n";
log(`\nunsanitized: ${unsanitizedString}`);
log(`\nsanitized: ${shadow.innerHTML}`);
});
The next click handler sets the target HTML using a custom sanitizer that allows only <div>
, <p>
, and <script>
elements.
const allowScriptButton = document.querySelector("#buttonAllowScript");
allowScriptButton.addEventListener("click", () => {
// Set the content of the element using a custom sanitizer
const sanitizer1 = new Sanitizer({
elements: ["div", "p", "script"],
});
shadow.setHTMLUnsafe(unsanitizedString, { sanitizer: sanitizer1 });
// Log HTML before sanitization and after being injected
logElement.textContent = "Sanitizer: {elements: ['div', 'p', 'script']}\n";
log(`\nunsanitized: ${unsanitizedString}`);
log(`\nsanitized: ${shadow.innerHTML}`);
});
Results
Click the "None" and "allowScript" buttons to see the effects of no sanitizer and a custom sanitizer, respectively.
When you click the "None" button, you should see that the input and output match, as no sanitizer is applied.
When you click the "allowScript" button the <script>
element is still present, but the <button>
element is removed.
With this approach you can create safe HTML, but you aren't forced to.
Specifications
Specification |
---|
HTML> # dom-shadowroot-sethtmlunsafe> |
Browser compatibility
Loading…