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/exports/vite/utils.js b/packages/kit/src/exports/vite/utils.js
index aaa33971bede..50d7ea802c37 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, escape_html_attr } 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/utils/escape.js b/packages/kit/src/utils/escape.js
index 543e1a13c0a5..ffc4d8c4de2a 100644
--- a/packages/kit/src/utils/escape.js
+++ b/packages/kit/src/utils/escape.js
@@ -44,3 +44,30 @@ export function escape_html_attr(str) {
return `"${escaped_str}"`;
}
+
+const ATTR_REGEX = /[&"<]/g;
+const CONTENT_REGEX = /[&<]/g;
+
+/**
+ * @template V
+ * @param {V} value
+ * @param {boolean} [is_attr]
+ */
+export function escape_html(value, is_attr) {
+ const str = String(value ?? '');
+
+ const pattern = is_attr ? ATTR_REGEX : CONTENT_REGEX;
+ pattern.lastIndex = 0;
+
+ let escaped = '';
+ let last = 0;
+
+ while (pattern.test(str)) {
+ const i = pattern.lastIndex - 1;
+ const ch = str[i];
+ escaped += str.substring(last, i) + (ch === '&' ? '&' : ch === '"' ? '"' : '<');
+ last = i + 1;
+ }
+
+ return escaped + str.substring(last);
+}