[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 <kfiiranet@gmail.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
Co-authored-by: Rich Harris <hello@rich-harris.dev>
pull/17698/head
Jay Harris 6 months ago committed by GitHub
parent 75e1992141
commit 6c15e711e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: Use `TrustedTypes` for HTML handling where supported

@ -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",

@ -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)) {

@ -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;
}

@ -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) {

@ -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

Loading…
Cancel
Save