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');
});