From 6c15e711e2dd5a312fc0f1c6b437718df4526d8e Mon Sep 17 00:00:00 2001 From: Jay Harris Date: Fri, 13 Feb 2026 15:34:59 +1300 Subject: [PATCH] [fix]: Add support for TrustedTypes in Svelte (#16271) ### Before submitting the PR, please make sure you do the following Resolves https://github.com/sveltejs/svelte/issues/14438 Resolves https://github.com/sveltejs/svelte/issues/10826 This PR makes it possible to use Svelte on pages which require `TrustedTypes` support via their CSP by wrapping assignments to `innerHTML` in a `TrustedTypePolicy` called `svelte-trusted-html` if the `TrustedTypes` API exists. Servers can allowlist the policy by setting `require-trusted-types-for 'script'; trusted-types svelte-trusted-html` in their `Content-Security-Policy` header. - [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [ ] Ideally, include a test that fails without this PR but passes with it. - [x] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting Note: I haven't run the tests since I don't have `pnpm` setup properly. I have tested that: 1. A project with a CSP fails with Tip of Tree Svelte 2. That project works when installing this revision of Svelte 3. The project (with this revision) works in Browsers with no `TrustedTypes` support (i.e. Firefox, Safari) - [ ] Run the tests with `pnpm test` and lint the project with `pnpm lint` My test project is here: https://github.com/fallaciousreasoning/svelte-tt-test/blob/master/src/routes/%2Bpage.server.js The only changes to the default project is adding the CSP in `src/routes/page.server.js` --------- Co-authored-by: 7nik Co-authored-by: Rich Harris Co-authored-by: Rich Harris --- .changeset/witty-lies-happen.md | 5 ++++ packages/svelte/package.json | 1 + .../src/internal/client/dom/blocks/snippet.js | 2 +- .../src/internal/client/dom/reconciler.js | 25 +++++++++++++++++-- .../src/internal/client/dom/template.js | 4 +-- pnpm-lock.yaml | 8 ++++++ 6 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 .changeset/witty-lies-happen.md diff --git a/.changeset/witty-lies-happen.md b/.changeset/witty-lies-happen.md new file mode 100644 index 0000000000..e176103226 --- /dev/null +++ b/.changeset/witty-lies-happen.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: Use `TrustedTypes` for HTML handling where supported diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c47e1ddbcd..452187662c 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -170,6 +170,7 @@ "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 0c4948aca0..7da7e5072d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -83,7 +83,7 @@ export function createRawSnippet(fn) { hydrate_next(); } else { var html = snippet.render().trim(); - var fragment = create_fragment_from_html(html); + var fragment = create_fragment_from_html(html, true); element = /** @type {Element} */ (get_first_child(fragment)); if (DEV && (get_next_sibling(element) !== null || element.nodeType !== ELEMENT_NODE)) { diff --git a/packages/svelte/src/internal/client/dom/reconciler.js b/packages/svelte/src/internal/client/dom/reconciler.js index 54f2e23c6e..1f4750ce8f 100644 --- a/packages/svelte/src/internal/client/dom/reconciler.js +++ b/packages/svelte/src/internal/client/dom/reconciler.js @@ -1,8 +1,29 @@ +/** @import {} from 'trusted-types' */ + import { create_element } from './operations.js'; +const policy = /* @__PURE__ */ globalThis?.window?.trustedTypes?.createPolicy( + 'svelte-trusted-html', + { + /** @param {string} html */ + createHTML: (html) => { + return html; + } + } +); + /** @param {string} html */ -export function create_fragment_from_html(html) { +function create_trusted_html(html) { + return /** @type {string} */ (policy?.createHTML(html) ?? html); +} + +/** + * @param {string} html + * @param {boolean} trusted + */ +export function create_fragment_from_html(html, trusted = false) { var elem = create_element('template'); - elem.innerHTML = html.replaceAll('', ''); // XHTML compliance + html = html.replaceAll('', ''); // XHTML compliance + elem.innerHTML = trusted ? create_trusted_html(html) : html; return elem.content; } diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index a5d0405d0a..7e12ff433f 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -70,7 +70,7 @@ export function from_html(content, flags) { } if (node === undefined) { - node = create_fragment_from_html(has_start ? content : '' + content); + node = create_fragment_from_html(has_start ? content : '' + content, true); if (!is_fragment) node = /** @type {TemplateNode} */ (get_first_child(node)); } @@ -118,7 +118,7 @@ function from_namespace(content, flags, ns = 'svg') { } if (!node) { - var fragment = /** @type {DocumentFragment} */ (create_fragment_from_html(wrapped)); + var fragment = /** @type {DocumentFragment} */ (create_fragment_from_html(wrapped, true)); var root = /** @type {Element} */ (get_first_child(fragment)); if (is_fragment) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48c2a52cc5..1d3427fae0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: '@types/estree': specifier: ^1.0.5 version: 1.0.8 + '@types/trusted-types': + specifier: ^2.0.7 + version: 2.0.7 acorn: specifier: ^8.12.1 version: 8.15.0 @@ -933,6 +936,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@typescript-eslint/eslint-plugin@8.55.0': resolution: {integrity: sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3189,6 +3195,8 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/trusted-types@2.0.7': {} + '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.5.4))(eslint@10.0.0)(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.12.2