From d338d4635a7fd947ba5112df6ee632c4a0979438 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sun, 24 Nov 2024 19:03:15 -0800 Subject: [PATCH] fix: escape values included in dev 404 page (#13039) --- .changeset/five-maps-yawn.md | 5 ++ packages/kit/src/core/postbuild/prerender.js | 9 +-- packages/kit/src/exports/vite/utils.js | 11 +++- packages/kit/src/runtime/server/page/csp.js | 4 +- .../src/runtime/server/page/serialize_data.js | 4 +- packages/kit/src/utils/escape.js | 56 ++++++++++++------- packages/kit/src/utils/escape.spec.js | 20 +++---- 7 files changed, 69 insertions(+), 40 deletions(-) create mode 100644 .changeset/five-maps-yawn.md diff --git a/.changeset/five-maps-yawn.md b/.changeset/five-maps-yawn.md new file mode 100644 index 000000000000..b4df5751e4f9 --- /dev/null +++ b/.changeset/five-maps-yawn.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: escape values included in dev 404 page diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index f2f511d4a98a..107448c338ff 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -4,7 +4,7 @@ import { pathToFileURL } from 'node:url'; import { installPolyfills } from '../../exports/node/polyfills.js'; import { mkdirp, posixify, walk } from '../../utils/filesystem.js'; import { decode_uri, is_root_relative, resolve } from '../../utils/url.js'; -import { escape_html_attr } from '../../utils/escape.js'; +import { escape_html } from '../../utils/escape.js'; import { logger } from '../utils.js'; import { load_config } from '../config/index.js'; import { get_route_segments } from '../../utils/routing.js'; @@ -359,9 +359,10 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) { dest, `` + )};` ); written.add(file); diff --git a/packages/kit/src/exports/vite/utils.js b/packages/kit/src/exports/vite/utils.js index aaa33971bede..a4ff95178803 100644 --- a/packages/kit/src/exports/vite/utils.js +++ b/packages/kit/src/exports/vite/utils.js @@ -3,6 +3,7 @@ import { loadEnv } from 'vite'; import { posixify } from '../../utils/filesystem.js'; import { negotiate } from '../../utils/http.js'; import { filter_private_env, filter_public_env } from '../../utils/env.js'; +import { escape_html } from '../../utils/escape.js'; /** * Transforms kit.alias to a valid vite.resolve.alias array. @@ -89,11 +90,17 @@ export function not_found(req, res, base) { if (type === 'text/html') { res.setHeader('Content-Type', 'text/html'); res.end( - `The server is configured with a public base URL of ${base} - did you mean to visit ${prefixed} instead?` + `The server is configured with a public base URL of ${escape_html( + base + )} - did you mean to visit ${escape_html( + prefixed + )} instead?` ); } else { res.end( - `The server is configured with a public base URL of ${base} - did you mean to visit ${prefixed} instead?` + `The server is configured with a public base URL of ${escape_html( + base + )} - did you mean to visit ${escape_html(prefixed)} instead?` ); } } diff --git a/packages/kit/src/runtime/server/page/csp.js b/packages/kit/src/runtime/server/page/csp.js index 336d50261ad3..7596385ba8a5 100644 --- a/packages/kit/src/runtime/server/page/csp.js +++ b/packages/kit/src/runtime/server/page/csp.js @@ -1,4 +1,4 @@ -import { escape_html_attr } from '../../../utils/escape.js'; +import { escape_html } from '../../../utils/escape.js'; import { base64, sha256 } from './crypto.js'; const array = new Uint8Array(16); @@ -300,7 +300,7 @@ class CspProvider extends BaseProvider { return; } - return ``; + return ``; } } diff --git a/packages/kit/src/runtime/server/page/serialize_data.js b/packages/kit/src/runtime/server/page/serialize_data.js index f879a50b3156..7c2e552d4fe0 100644 --- a/packages/kit/src/runtime/server/page/serialize_data.js +++ b/packages/kit/src/runtime/server/page/serialize_data.js @@ -1,4 +1,4 @@ -import { escape_html_attr } from '../../../utils/escape.js'; +import { escape_html } from '../../../utils/escape.js'; import { hash } from '../../hash.js'; /** @@ -70,7 +70,7 @@ export function serialize_data(fetched, filter, prerendering = false) { const attrs = [ 'type="application/json"', 'data-sveltekit-fetched', - `data-url=${escape_html_attr(fetched.url)}` + `data-url="${escape_html(fetched.url, true)}"` ]; if (fetched.is_b64) { diff --git a/packages/kit/src/utils/escape.js b/packages/kit/src/utils/escape.js index 543e1a13c0a5..a73fd951daf8 100644 --- a/packages/kit/src/utils/escape.js +++ b/packages/kit/src/utils/escape.js @@ -6,41 +6,57 @@ const escape_html_attr_dict = { '&': '&', '"': '"' + // Svelte also escapes < because the escape function could be called inside a `noscript` there + // https://github.com/sveltejs/svelte/security/advisories/GHSA-8266-84wp-wv5c + // However, that doesn't apply in SvelteKit }; +/** + * @type {Record} + */ +const escape_html_dict = { + '&': '&', + '<': '<' +}; + +const surrogates = // high surrogate without paired low surrogate + '[\\ud800-\\udbff](?![\\udc00-\\udfff])|' + + // a valid surrogate pair, the only match with 2 code units + // we match it so that we can match unpaired low surrogates in the same pass + // TODO: use lookbehind assertions once they are widely supported: (?...`; + * @param {boolean} [is_attr] + * @returns {string} escaped string + * @example const html = `...`; */ -export function escape_html_attr(str) { - const escaped_str = str.replace(escape_html_attr_regex, (match) => { +export function escape_html(str, is_attr) { + const dict = is_attr ? escape_html_attr_dict : escape_html_dict; + const escaped_str = str.replace(is_attr ? escape_html_attr_regex : escape_html_regex, (match) => { if (match.length === 2) { // valid surrogate pair return match; } - return escape_html_attr_dict[match] ?? `&#${match.charCodeAt(0)};`; + return dict[match] ?? `&#${match.charCodeAt(0)};`; }); - return `"${escaped_str}"`; + return escaped_str; } diff --git a/packages/kit/src/utils/escape.spec.js b/packages/kit/src/utils/escape.spec.js index 15d3bfe62f0a..3c4e6a042a86 100644 --- a/packages/kit/src/utils/escape.spec.js +++ b/packages/kit/src/utils/escape.spec.js @@ -1,19 +1,19 @@ import { assert, test } from 'vitest'; -import { escape_html_attr } from './escape.js'; +import { escape_html } from './escape.js'; test('escape_html_attr escapes special attribute characters', () => { assert.equal( - escape_html_attr('some "values" are &special here, aren\'t.'), - '"some "values" are &special here, aren\'t."' + escape_html('some "values" are &special here, aren\'t.', true), + "some "values" are &special here, aren't." ); }); test('escape_html_attr escapes invalid surrogates', () => { - assert.equal(escape_html_attr('\ud800\udc00'), '"\ud800\udc00"'); - assert.equal(escape_html_attr('\ud800'), '"�"'); - assert.equal(escape_html_attr('\udc00'), '"�"'); - assert.equal(escape_html_attr('\udc00\ud800'), '"��"'); - assert.equal(escape_html_attr('\ud800\ud800\udc00'), '"�\ud800\udc00"'); - assert.equal(escape_html_attr('\ud800\udc00\udc00'), '"\ud800\udc00�"'); - assert.equal(escape_html_attr('\ud800\ud800\udc00\udc00'), '"�\ud800\udc00�"'); + assert.equal(escape_html('\ud800\udc00', true), '\ud800\udc00'); + assert.equal(escape_html('\ud800', true), '�'); + assert.equal(escape_html('\udc00', true), '�'); + assert.equal(escape_html('\udc00\ud800', true), '��'); + assert.equal(escape_html('\ud800\ud800\udc00', true), '�\ud800\udc00'); + assert.equal(escape_html('\ud800\udc00\udc00', true), '\ud800\udc00�'); + assert.equal(escape_html('\ud800\ud800\udc00\udc00', true), '�\ud800\udc00�'); });