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