Browse Source

feat: setInnerHtmlUnsafe() function for Trusted Types API

Sv443 7 months ago
parent
commit
7303aa2dc7
6 changed files with 83 additions and 40 deletions
  1. 5 0
      .changeset/sixty-guests-teach.md
  2. 5 0
      .changeset/soft-bikes-cheer.md
  3. 31 39
      README.md
  4. 24 1
      lib/dom.ts
  5. 17 0
      lib/types.ts
  6. 1 0
      package.json

+ 5 - 0
.changeset/sixty-guests-teach.md

@@ -0,0 +1,5 @@
+---
+"@sv443-network/userutils": patch
+---
+
+Made `addGlobalStyle()` use the new `setInnerHtmlUnsafe()` to fix the error "This document requires 'TrustedHTML' assignment" on Chromium-based browsers

+ 5 - 0
.changeset/soft-bikes-cheer.md

@@ -0,0 +1,5 @@
+---
+"@sv443-network/userutils": minor
+---
+
+Added `setInnerHtmlUnsafe()` for setting innerHTML unsanitized using a [Trusted Types policy](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API)

+ 31 - 39
README.md

@@ -37,6 +37,7 @@ View the documentation of previous major releases:
     - [`isScrollable()`](#isscrollable) - check if an element has a horizontal or vertical scroll bar
     - [`observeElementProp()`](#observeelementprop) - observe changes to an element's property that can't be observed with MutationObserver
     - [`getSiblingsFrame()`](#getsiblingsframe) - returns a frame of an element's siblings, with a given alignment and size
+    - [`setInnerHtmlUnsafe()`](#setinnerhtmlunsafe) - set the innerHTML of an element using a [Trusted Types policy](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API) without sanitizing or escaping it
   - [**Math:**](#math)
     - [`clamp()`](#clamp) - constrain a number between a min and max value
     - [`mapRange()`](#maprange) - map a number from one range to the same spot in another range
@@ -450,10 +451,8 @@ document.addEventListener("DOMContentLoaded", () => {
   fooObserver.enable();
 });
 ```
-
 </details>
 
-
 <br>
 
 ### getUnsafeWindow()
@@ -481,7 +480,6 @@ const mouseEvent = new MouseEvent("mousemove", {
 
 document.body.dispatchEvent(mouseEvent);
 ```
-
 </details>
 
 <br>
@@ -509,7 +507,6 @@ newParent.href = "https://example.org/";
 
 addParent(element, newParent);
 ```
-
 </details>
 
 <br>
@@ -537,7 +534,6 @@ document.addEventListener("DOMContentLoaded", () => {
   `);
 });
 ```
-
 </details>
 
 <br>
@@ -569,7 +565,6 @@ preloadImages([
     console.error("Couldn't preload all images. Results:", results);
   });
 ```
-
 </details>
 
 <br>
@@ -596,7 +591,6 @@ document.querySelector("#my-button").addEventListener("click", () => {
   openInNewTab("https://example.org/", true);
 });
 ```
-
 </details>
 
 <br>
@@ -631,7 +625,6 @@ interceptEvent(document.body, "click", (event) => {
   return false; // allow all other click events through
 });
 ```
-
 </details>
 
 <br>
@@ -661,7 +654,6 @@ import { interceptWindowEvent } from "@sv443-network/userutils";
 // as no predicate is specified, all events will be discarded by default
 interceptWindowEvent("beforeunload");
 ```
-
 </details>
 
 <br>
@@ -686,7 +678,6 @@ const { horizontal, vertical } = isScrollable(element);
 console.log("Element has a horizontal scroll bar:", horizontal);
 console.log("Element has a vertical scroll bar:", vertical);
 ```
-
 </details>
 
 <br>
@@ -739,7 +730,6 @@ observeElementProp(myInput, "value", (oldValue, newValue) => {
   console.log("Value changed from", oldValue, "to", newValue);
 });
 ```
-
 </details>
 
 <br>
@@ -862,7 +852,37 @@ const allBelowExcl = getSiblingsFrame(refElement, Infinity, "bottom", false);
 // <div>5</div>             │ frame
 // <div>6</div>          ◄──┘
 ```
+</details>
+
+<br>
 
+### setInnerHtmlUnsafe()
+Usage:  
+```ts
+setInnerHtmlUnsafe(element: Element, html: string): Element
+```
+  
+Sets the innerHTML property of the provided element without any sanitation or validation.  
+Makes use of the [Trusted Types API](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API) to trick the browser into thinking the HTML is safe.  
+Use this function if the page makes use of the CSP directive `require-trusted-types-for 'script'` and throws a "This document requires 'TrustedHTML' assignment" error on Chromium-based browsers.  
+If the browser doesn't support Trusted Types, this function will fall back to regular innerHTML assignment.  
+  
+⚠️ This function does not perform any sanitization, it only tricks the browser into thinking the HTML is safe and should thus be used with utmost caution, as it can easily cause XSS vulnerabilities!  
+A much better way of doing this is by using the [DOMPurify](https://github.com/cure53/DOMPurify#what-about-dompurify-and-trusted-types) library to create your own Trusted Types policy that *actually* sanitizes the HTML and prevents (most) XSS attack vectors.  
+You can also find more info [here.](https://web.dev/articles/trusted-types#library)  
+  
+<details><summary><b>Example - click to view</b></summary>
+
+```ts
+import { setInnerHtmlUnsafe } from "@sv443-network/userutils";
+
+const myElement = document.querySelector("#my-element");
+setInnerHtmlUnsafe(myElement, "<img src='https://picsum.photos/100/100' />");   // hardcoded value, so no XSS risk
+
+const myXssElement = document.querySelector("#my-xss-element");
+const userModifiableVariable = `<img onerror="alert('XSS!')" src="invalid" />`; // let's pretend this came from user input
+setInnerHtmlUnsafe(myXssElement, userModifiableVariable);                       // <- uses a user-modifiable variable, so big XSS risk!
+```
 </details>
 
 <br><br>
@@ -892,7 +912,6 @@ clamp(99999, 0, 10); // 10
 clamp(-99999, -Infinity, 0); // -99999
 clamp(99999, 0, Infinity);   // 99999
 ```
-
 </details>
 
 <br>
@@ -923,7 +942,6 @@ mapRange(5, 0, 10, 0, 50);  // 25
 // for example, if 4 files of a total of 13 were downloaded:
 mapRange(4, 0, 13, 0, 100); // 30.76923076923077
 ```
-
 </details>
 
 <br>
@@ -947,7 +965,6 @@ randRange(0, 10);  // 4
 randRange(10, 20); // 17
 randRange(10);     // 7
 ```
-
 </details>
 
 <br><br>
@@ -1115,7 +1132,6 @@ async function init() {
 
 init();
 ```
-
 </details>
 
 <br>
@@ -1376,7 +1392,6 @@ fooDialog.on("close", () => {
 
 fooDialog.open();
 ```
-
 </details>
 
 <br>
@@ -1458,7 +1473,6 @@ myInstance.emit("baz", "hello from the outside");
 
 myInstance.unsubscribeAll();
 ```
-
 </details>
 
 <br>
@@ -1528,7 +1542,6 @@ autoPlural("apple", [1, 2]); // "apples"
 const items = [1, 2, 3, 4, "foo", "bar"];
 console.log(`Found ${items.length} ${autoPlural("item", items)}`); // "Found 6 items"
 ```
-
 </details>
 
 <br>
@@ -1552,7 +1565,6 @@ async function run() {
   console.log("World");
 }
 ```
-
 </details>
 
 <br>
@@ -1598,7 +1610,6 @@ const myFunc = debounce((event) => {
 
 document.body.addEventListener("scroll", myFunc);
 ```
-
 </details>
 
 <br>
@@ -1633,7 +1644,6 @@ fetchAdvanced("https://jokeapi.dev/joke/Any?safe-mode", {
   console.error("Fetch error:", err);
 });
 ```
-
 </details>
 
 <br>
@@ -1661,7 +1671,6 @@ insertValues("Testing %1", { toString: () => "foo" });      // "Testing foo"
 const values = ["foo", "bar", "baz"];
 insertValues("Testing %1, %2, %3 and %4", ...values); // "Testing foo, bar and baz and %4"
 ```
-
 </details>
 
 <br>
@@ -1702,7 +1711,6 @@ const barDeflate = await compress("Hello, World!".repeat(20), "deflate");
 console.log(fooDeflate); // "eJzzSM3JyddRCM8vyklRBAAfngRq"
 console.log(barDeflate); // "eJzzSM3JyddRCM8vyklR9BiZHAAIEVg1"
 ```
-
 </details>
 
 <br>
@@ -1733,7 +1741,6 @@ const decompressed = await decompress(compressed, "gzip");
 
 console.log(decompressed); // "Hello, World!"
 ```
-
 </details>
 
 <br>
@@ -1800,7 +1807,6 @@ randomId(10, 2);  // "1010001101"       (length 10, radix 2)
 randomId(10, 10); // "0183428506"       (length 10, radix 10)
 randomId(10, 36); // "z46jfpa37r"       (length 10, radix 36)
 ```
-
 </details>
 
 <br><br>
@@ -1825,7 +1831,6 @@ import { randomItem } from "@sv443-network/userutils";
 randomItem(["foo", "bar", "baz"]); // "bar"
 randomItem([ ]);                   // undefined
 ```
-
 </details>
 
 <br>
@@ -1852,7 +1857,6 @@ const [item, index] = randomItemIndex(["foo", "bar", "baz"]); // ["bar", 1]
 // or if you only want the index:
 const [, index] = randomItemIndex(["foo", "bar", "baz"]); // 1
 ```
-
 </details>
 
 <br>
@@ -1875,7 +1879,6 @@ const arr = ["foo", "bar", "baz"];
 takeRandomItem(arr); // "bar"
 console.log(arr);    // ["foo", "baz"]
 ```
-
 </details>
 
 <br>
@@ -1901,7 +1904,6 @@ console.log(randomizeArray(foo)); // [4, 5, 2, 1, 6, 3]
 
 console.log(foo); // [1, 2, 3, 4, 5, 6] - original array is not mutated
 ```
-
 </details>
 
 <br><br>
@@ -1954,7 +1956,6 @@ tr.setLanguage("de");
 console.log(tr("welcome"));              // "Willkommen"
 console.log(tr("welcome_name", "John")); // "Willkommen, John"
 ```
-
 </details>
 
 <br>
@@ -2053,7 +2054,6 @@ console.log(tr(pl("cart_items_added", items), items.length)); // "Added 1 item t
 items.push("bar");
 console.log(tr(pl("cart_items_added", items), items.length)); // "Added 2 items to the cart"
 ```
-
 </details>
 
 <br>
@@ -2103,7 +2103,6 @@ hexToRgb("#ff0000"); // [255, 0, 0]
 hexToRgb("0032ef");  // [0, 50, 239]
 hexToRgb("#0f0");    // [0, 255, 0]
 ```
-
 </details>
 
 <br>
@@ -2127,7 +2126,6 @@ rgbToHex(255, 0, 0);             // "#ff0000"
 rgbToHex(255, 0, 0, false);      // "ff0000"
 rgbToHex(255, 0, 0, true, true); // "#FF0000"
 ```
-
 </details>
 
 <br>
@@ -2151,7 +2149,6 @@ lightenColor("#ff0000", 20);              // "#ff3333"
 lightenColor("rgb(0, 255, 0)", 50);       // "rgb(128, 255, 128)"
 lightenColor("rgba(0, 255, 0, 0.5)", 50); // "rgba(128, 255, 128, 0.5)"
 ```
-
 </details>
 
 <br>
@@ -2175,7 +2172,6 @@ darkenColor("#ff0000", 20);              // "#cc0000"
 darkenColor("rgb(0, 255, 0)", 50);       // "rgb(0, 128, 0)"
 darkenColor("rgba(0, 255, 0, 0.5)", 50); // "rgba(0, 128, 0, 0.5)"
 ```
-
 </details>
 
 <br><br>
@@ -2216,7 +2212,6 @@ logSomething(fooObject); // "Log: hello world"
 
 logSomething(barObject); // Type error
 ```
-
 </details>
 
 <br>
@@ -2247,7 +2242,6 @@ function somethingElse(array: NonEmptyArray) {
 
 logFirstItem(["04abc", "69"]); // 4
 ```
-
 </details>
 
 <br>
@@ -2272,7 +2266,6 @@ function convertToNumber<T extends string>(str: NonEmptyString<T>) {
 convertToNumber("04abc"); // "4"
 convertToNumber("");      // type error: Argument of type 'string' is not assignable to parameter of type 'never'
 ```
-
 </details>
 
 <br>
@@ -2300,7 +2293,6 @@ foo("a"); // included in autocomplete, no type error
 foo("");  // *not* included in autocomplete, still no type error
 foo(1);   // type error: Argument of type '1' is not assignable to parameter of type 'LooseUnion<"a" | "b" | "c">'
 ```
-
 </details>
 
 <br><br><br><br>

+ 24 - 1
lib/dom.ts

@@ -1,3 +1,5 @@
+import type { TrustedTypesPolicy } from "./types.js";
+
 /**
  * Returns `unsafeWindow` if the `@grant unsafeWindow` is given, otherwise falls back to the regular `window`
  */
@@ -35,7 +37,7 @@ export function addParent<TElem extends Element, TParentElem extends Element>(el
  */
 export function addGlobalStyle(style: string): HTMLStyleElement {
   const styleElem = document.createElement("style");
-  styleElem.innerHTML = style;
+  setInnerHtmlUnsafe(styleElem, style);
   document.head.appendChild(styleElem);
   return styleElem;
 }
@@ -232,3 +234,24 @@ export function getSiblingsFrame<
 
   return [] as TSibling[];
 }
+
+let ttPolicy: TrustedTypesPolicy | undefined;
+
+/**
+ * Sets the innerHTML property of the provided element without any sanitation or validation.  
+ * Uses a [Trusted Types policy](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API) on Chromium-based browsers to trick the browser into thinking the HTML is safe.  
+ * Use this if the page makes use of the CSP directive `require-trusted-types-for 'script'` and throws a "This document requires 'TrustedHTML' assignment" error on Chromium-based browsers.  
+ *   
+ * ⚠️ This function does not perform any sanitization and should thus be used with utmost caution, as it can easily lead to XSS vulnerabilities!
+ */
+export function setInnerHtmlUnsafe<TElement extends Element = HTMLElement>(element: TElement, html: string): TElement {
+  if(!ttPolicy && typeof window?.trustedTypes?.createPolicy === "function") {
+    ttPolicy = window.trustedTypes.createPolicy("_uu_set_innerhtml_unsafe", {
+      createHTML: (unsafeHtml: string) => unsafeHtml,
+    });
+  }
+
+  element.innerHTML = ttPolicy?.createHTML?.(html) ?? html;
+
+  return element;
+}

+ 17 - 0
lib/types.ts

@@ -1,3 +1,20 @@
+//#region shims
+
+export type TrustedTypesPolicy = {
+  createHTML?: (dirty: string) => string;
+};
+
+declare global {
+  interface Window {
+    // poly-shim for the new Trusted Types API
+    trustedTypes: {
+      createPolicy(name: string, policy: TrustedTypesPolicy): TrustedTypesPolicy;
+    };
+  }
+}
+
+//#region UU types
+
 /** Represents any value that is either a string itself or can be converted to one (implicitly and explicitly) because it has a toString() method */
 export type Stringifiable = string | { toString(): string };
 

+ 1 - 0
package.json

@@ -15,6 +15,7 @@
     "build": "npm run build-common -- && npm run build-types",
     "post-build-global": "npm run node-ts -- ./tools/post-build-global.mts",
     "dev": "npm run build-common -- --sourcemap --watch --onSuccess \"npm run build-types && echo Finished building.\"",
+    "dev-all": "npm run build-all -- --watch",
     "update-jsr-version": "npm run node-ts -- ./tools/update-jsr-version.mts",
     "publish-package": "changeset publish",
     "publish-package-jsr": "npm run update-jsr-version && npx jsr publish",