From 533844944d365bb7367825496ca693b95c62e89f Mon Sep 17 00:00:00 2001 From: Hamish Willee Date: Tue, 6 Jan 2026 13:15:32 +1100 Subject: [PATCH 1/4] TrustedTypes: Indirect sinks --- .../en-us/web/api/trusted_types_api/index.md | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/files/en-us/web/api/trusted_types_api/index.md b/files/en-us/web/api/trusted_types_api/index.md index caafbb17444865d..81b4604ad945980 100644 --- a/files/en-us/web/api/trusted_types_api/index.md +++ b/files/en-us/web/api/trusted_types_api/index.md @@ -138,8 +138,8 @@ element.innerHTML = userInput; This section provides a list of "direct" injection sink interfaces. -Note that there are cases where untrusted strings may be "indirectly injected", such as when an untrusted string is added as the child node of a script element, and then the element is added to the document. -These cases are evaluated the untrusted script is added to the document. +These are the API properties and methods which perform trusted type checks when they are evaluated. +They can be passed trusted types (`TrustedHTML`, `TrustedScript`, or `TrustedScriptURL`) as well as strings, and must be passed trusted types when trusted type enforcement is enabled. #### TrustedHTML @@ -181,6 +181,31 @@ These cases are evaluated the untrusted script is added to the document. - `url` argument to [`Worker()` constructor](/en-US/docs/Web/API/Worker/Worker#url) - `url` argument to [`SharedWorker()` constructor](/en-US/docs/Web/API/SharedWorker/SharedWorker#url) +### Indirect injection sinks + +Indirect injection sinks are those where untrusted strings are injected into the DOM via an intermediate mechanism that doesn't trigger script execution, and then evaluated. +These kinds of sinks bypass the trusted type checks that have been added to the direct injection sinks. + +The following code shows how this might work (note that there are many other mechanisms for indirect injection). +First a text node is created using a string provided by a user, and then a {{htmlelement("script")}} element is constructed and the text node is appended as a child element. +Next the script element is added to the DOM as a child of the {{htmlelement("body")}} element, potentially allowing any scripts defined in the original string to be run. + +```js +// Create a text node +const unstrustedString = "/* Potentially malcious JavaScript code */"; +const textNode = document.createTextNode(unstrustedString); + +// Create a script element and add the text node +const script = document.createElement("script"); +script.appendChild(textNode); +document.body.appendChild(script); +``` + +Because a text node isn't always used in a context where the text might be used as an injection sink, it does enforce trusted types. + +Instead, browsers that are enforcing trusted types will run trusted type checks when indirect injection sinks are is injected into the DOM (in the example above, when `appendChild()` is called). +This will cause an exception if the text node was constructed with a string rather than an appropriate trusted type. + ### Cross-browser support for trusted types The Trusted Types API is not yet available in all modern browsers, but it is usable everywhere today thanks to [compatibility aids created by the W3C](https://github.com/w3c/trusted-types/tree/main?tab=readme-ov-file#polyfill). From 550deb41c4fb386ba54e6bfda5c5c71cf71a0f1a Mon Sep 17 00:00:00 2001 From: Hamish Willee Date: Fri, 9 Jan 2026 16:30:11 +1100 Subject: [PATCH 2/4] Apply suggestions from code review --- files/en-us/web/api/trusted_types_api/index.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/files/en-us/web/api/trusted_types_api/index.md b/files/en-us/web/api/trusted_types_api/index.md index 81b4604ad945980..1ee33d79a89a206 100644 --- a/files/en-us/web/api/trusted_types_api/index.md +++ b/files/en-us/web/api/trusted_types_api/index.md @@ -139,7 +139,7 @@ element.innerHTML = userInput; This section provides a list of "direct" injection sink interfaces. These are the API properties and methods which perform trusted type checks when they are evaluated. -They can be passed trusted types (`TrustedHTML`, `TrustedScript`, or `TrustedScriptURL`) as well as strings, and must be passed trusted types when trusted type enforcement is enabled. +They can be passed trusted types (`TrustedHTML`, `TrustedScript`, or `TrustedScriptURL`) as well as strings, and must be passed trusted types when trusted type enforcement is enabled and no default policy is defined. #### TrustedHTML @@ -192,7 +192,7 @@ Next the script element is added to the DOM as a child of the {{htmlelement("bod ```js // Create a text node -const unstrustedString = "/* Potentially malcious JavaScript code */"; +const unstrustedString = "/* Potentially malicious JavaScript code */"; const textNode = document.createTextNode(unstrustedString); // Create a script element and add the text node @@ -201,9 +201,10 @@ script.appendChild(textNode); document.body.appendChild(script); ``` -Because a text node isn't always used in a context where the text might be used as an injection sink, it does enforce trusted types. +Because a text node won't always used in contexts where its text is executable, trusted type checking isn't performed when the untrusted string is used to create the node. -Instead, browsers that are enforcing trusted types will run trusted type checks when indirect injection sinks are is injected into the DOM (in the example above, when `appendChild()` is called). +Instead, browsers run the trusted type checks when potentially untrustworthy code is about to be executed. +In the example above, this is when `document.body.appendChild(script)` is called to add the script element to the DOM. This will cause an exception if the text node was constructed with a string rather than an appropriate trusted type. ### Cross-browser support for trusted types From aacb1fe75c754b797019f20939d4c06e348e2e1e Mon Sep 17 00:00:00 2001 From: Hamish Willee Date: Fri, 30 Jan 2026 15:59:45 +1100 Subject: [PATCH 3/4] Update following feedback --- .../en-us/web/api/trusted_types_api/index.md | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/files/en-us/web/api/trusted_types_api/index.md b/files/en-us/web/api/trusted_types_api/index.md index 1ee33d79a89a206..045bc00c405efb7 100644 --- a/files/en-us/web/api/trusted_types_api/index.md +++ b/files/en-us/web/api/trusted_types_api/index.md @@ -183,29 +183,60 @@ They can be passed trusted types (`TrustedHTML`, `TrustedScript`, or `TrustedScr ### Indirect injection sinks -Indirect injection sinks are those where untrusted strings are injected into the DOM via an intermediate mechanism that doesn't trigger script execution, and then evaluated. -These kinds of sinks bypass the trusted type checks that have been added to the direct injection sinks. +_Indirect injection sinks_ are sinks where untrusted strings are injected into the DOM via an intermediate mechanism that doesn't accept or enforce trusted types. +These differ from the "direct" [Injection sink interfaces](#injection_sink_interfaces) listed in the previous section, which run trusted type checks on injected strings when they are called. -The following code shows how this might work (note that there are many other mechanisms for indirect injection). +For example, the following code sets script element source indirectly. First a text node is created using a string provided by a user, and then a {{htmlelement("script")}} element is constructed and the text node is appended as a child element. -Next the script element is added to the DOM as a child of the {{htmlelement("body")}} element, potentially allowing any scripts defined in the original string to be run. +Next the script element is added to the document as a child of the {{htmlelement("body")}} element — at this point scripts defined in the original string may be executed. ```js // Create a text node -const unstrustedString = "/* Potentially malicious JavaScript code */"; -const textNode = document.createTextNode(unstrustedString); +const untrustedString = + "console.log('A potentially malicious script from an untrusted source!');"; +const textNode = document.createTextNode(untrustedString); -// Create a script element and add the text node +// Create a script element and append the text node const script = document.createElement("script"); script.appendChild(textNode); + +// Add the script into the document, where it can run document.body.appendChild(script); ``` -Because a text node won't always used in contexts where its text is executable, trusted type checking isn't performed when the untrusted string is used to create the node. +When the text node is created there is no reason for the browser to assume it is intended to be used as a trusted type source, so trusted types are serialized to string, and are not enforced. + +Instead, browsers run the checks when the script element becomes executable — i.e., in this example, when `document.body.appendChild(script)` is called to add the script element to the document. + +The browser will first check if the string used as the script content is trusted. +Whether the string can be trusted depends on how it was set as the {{htmlelement("script")}} source. + + + +The source string is trusted if the script was modified using the following APIs: + +- Script source set via {{domxref("TrustedScript")}} sink on {{domxref("HTMLScriptElement.innerText")}}, {{domxref("HTMLScriptElement.textContent")}}, {{domxref("HTMLScriptElement.text")}} +- Splitting script source via {{domxref("Text.splitText()")}} +- Normalizing script source via `Element.normalize()` + +The source string is not trusted if modified with the following APIs: + +- Setting script source via {{domxref("HTMLElement.innerText")}} +- Setting script source via {{domxref("Node.textContent")}} or {{domxref("Node.nodeValue")}} +- Script source set via {{domxref("TrustedHTML")}} sink {{domxref("Element.innerHTML")}} or {{domxref("Element.setHTMLUnsafe()")}} +- Script source cannot be set via {{domxref("Element.setHTML()")}}. +- Setting script source via {{domxref("CharacterData")}} property {{domxref("CharacterData.data","data")}} and methods {{domxref("CharacterData.appendData()","appendData()")}}, {{domxref("CharacterData.insertData()","insertData()")}}, {{domxref("CharacterData.replaceData()","replaceData()")}}, {{domxref("CharacterData.deleteData()")}}, {{domxref("CharacterData.before()","before()")}}, {{domxref("CharacterData.after()","after()")}}, {{domxref("CharacterData.remove()","remove()")}}, {{domxref("CharacterData.replaceWith()","replaceWith()")}} +- Setting script source via {{domxref("Node.appendChild()")}}, {{domxref("Node.insertBefore()")}}, {{domxref("Node.replaceChild()")}}, {{domxref("Node.removeChild()")}} +- Setting script source via {{domxref("Element")}} methods {{domxref("Element.prepend()","prepend()")}}, {{domxref("Element.append()","append()")}}, {{domxref("Element.replaceChildren()","replaceChildren()")}}, {{domxref("Element.moveBefore()","moveBefore()")}} +- Setting script source via {{domxref("TrustedHTML")}} sink `Node.insertAdjacentHTML()` +- Setting script source via `Node.insertAdjacentText()` +- Setting script source via {{domxref("Range.insertNode()")}} +- Setting script source via {{domxref("Range.deleteContents()")}} +- Cloning a script via {{domxref("Node.cloneNode()")}} +- Cloning a script via {{domxref("Range.cloneContents()")}} -Instead, browsers run the trusted type checks when potentially untrustworthy code is about to be executed. -In the example above, this is when `document.body.appendChild(script)` is called to add the script element to the DOM. -This will cause an exception if the text node was constructed with a string rather than an appropriate trusted type. +If the string is not trusted and trusted types are enforced, the browser will attempt to obtain a `TrustedScript` from a [default policy](#the_default_policy) to use for source instead. +If a default policy is not defined, or does not return a `TrustedScript`, the operation will throw an exception. ### Cross-browser support for trusted types From 15db67d9dd75795ea93d8cf9c8363ceba03747f6 Mon Sep 17 00:00:00 2001 From: Hamish Willee Date: Mon, 2 Feb 2026 10:19:12 +1100 Subject: [PATCH 4/4] Criteria for an operation to make source untrusted --- .../en-us/web/api/trusted_types_api/index.md | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/files/en-us/web/api/trusted_types_api/index.md b/files/en-us/web/api/trusted_types_api/index.md index 045bc00c405efb7..2b1651a2cbd09c9 100644 --- a/files/en-us/web/api/trusted_types_api/index.md +++ b/files/en-us/web/api/trusted_types_api/index.md @@ -209,31 +209,8 @@ When the text node is created there is no reason for the browser to assume it is Instead, browsers run the checks when the script element becomes executable — i.e., in this example, when `document.body.appendChild(script)` is called to add the script element to the document. The browser will first check if the string used as the script content is trusted. -Whether the string can be trusted depends on how it was set as the {{htmlelement("script")}} source. - - - -The source string is trusted if the script was modified using the following APIs: - -- Script source set via {{domxref("TrustedScript")}} sink on {{domxref("HTMLScriptElement.innerText")}}, {{domxref("HTMLScriptElement.textContent")}}, {{domxref("HTMLScriptElement.text")}} -- Splitting script source via {{domxref("Text.splitText()")}} -- Normalizing script source via `Element.normalize()` - -The source string is not trusted if modified with the following APIs: - -- Setting script source via {{domxref("HTMLElement.innerText")}} -- Setting script source via {{domxref("Node.textContent")}} or {{domxref("Node.nodeValue")}} -- Script source set via {{domxref("TrustedHTML")}} sink {{domxref("Element.innerHTML")}} or {{domxref("Element.setHTMLUnsafe()")}} -- Script source cannot be set via {{domxref("Element.setHTML()")}}. -- Setting script source via {{domxref("CharacterData")}} property {{domxref("CharacterData.data","data")}} and methods {{domxref("CharacterData.appendData()","appendData()")}}, {{domxref("CharacterData.insertData()","insertData()")}}, {{domxref("CharacterData.replaceData()","replaceData()")}}, {{domxref("CharacterData.deleteData()")}}, {{domxref("CharacterData.before()","before()")}}, {{domxref("CharacterData.after()","after()")}}, {{domxref("CharacterData.remove()","remove()")}}, {{domxref("CharacterData.replaceWith()","replaceWith()")}} -- Setting script source via {{domxref("Node.appendChild()")}}, {{domxref("Node.insertBefore()")}}, {{domxref("Node.replaceChild()")}}, {{domxref("Node.removeChild()")}} -- Setting script source via {{domxref("Element")}} methods {{domxref("Element.prepend()","prepend()")}}, {{domxref("Element.append()","append()")}}, {{domxref("Element.replaceChildren()","replaceChildren()")}}, {{domxref("Element.moveBefore()","moveBefore()")}} -- Setting script source via {{domxref("TrustedHTML")}} sink `Node.insertAdjacentHTML()` -- Setting script source via `Node.insertAdjacentText()` -- Setting script source via {{domxref("Range.insertNode()")}} -- Setting script source via {{domxref("Range.deleteContents()")}} -- Cloning a script via {{domxref("Node.cloneNode()")}} -- Cloning a script via {{domxref("Range.cloneContents()")}} +Any operation that allows the text source of a {{htmlelement("script")}} to be modified without explicitly setting a {{domxref("TrustedScript")}} makes it untrusted. +The {{domxref("Node.appendChild()")}} method used above is just one example (a number of others are listed in the WPT Live tests at ). If the string is not trusted and trusted types are enforced, the browser will attempt to obtain a `TrustedScript` from a [default policy](#the_default_policy) to use for source instead. If a default policy is not defined, or does not return a `TrustedScript`, the operation will throw an exception.