diff --git a/.changeset/late-foxes-clean.md b/.changeset/late-foxes-clean.md
new file mode 100644
index 000000000000..1ec0de03dc6b
--- /dev/null
+++ b/.changeset/late-foxes-clean.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+fixes shadow hydration escaping
diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js
index 4d6f8ac71ff4..67891e389f6c 100644
--- a/packages/kit/src/runtime/server/page/render.js
+++ b/packages/kit/src/runtime/server/page/render.js
@@ -2,7 +2,7 @@ import devalue from 'devalue';
import { readable, writable } from 'svelte/store';
import { coalesce_to_error } from '../../../utils/error.js';
import { hash } from '../../hash.js';
-import { escape_html_attr } from '../../../utils/escape.js';
+import { escape_html_attr, escape_json_in_html } from '../../../utils/escape.js';
import { s } from '../../../utils/misc.js';
import { create_prerendering_url_proxy } from './utils.js';
import { Csp, csp_ready } from './csp.js';
@@ -261,7 +261,7 @@ export async function render_response({
if (shadow_props) {
// prettier-ignore
- body += ``;
+ body += ``;
}
}
diff --git a/packages/kit/src/utils/escape.js b/packages/kit/src/utils/escape.js
index 020972dff6ae..4a52cbf83fb8 100644
--- a/packages/kit/src/utils/escape.js
+++ b/packages/kit/src/utils/escape.js
@@ -1,6 +1,5 @@
/** @type {Record} */
-const escape_json_string_in_html_dict = {
- '"': '\\"',
+const escape_json_in_html_dict = {
'<': '\\u003C',
'>': '\\u003E',
'/': '\\u002F',
@@ -15,7 +14,24 @@ const escape_json_string_in_html_dict = {
'\u2029': '\\u2029'
};
-/** @param {string} str */
+/** @type {Record} */
+const escape_json_string_in_html_dict = {
+ '"': '\\"',
+ ...escape_json_in_html_dict
+};
+
+/**
+ * Escape a stringified JSON object that's going to be embedded in a `'
+ };
+ return {
+ body: { user }
+ };
+}
diff --git a/packages/kit/test/apps/basics/src/routes/xss/shadow.svelte b/packages/kit/test/apps/basics/src/routes/xss/shadow.svelte
new file mode 100644
index 000000000000..8ad2b30f8e4f
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/xss/shadow.svelte
@@ -0,0 +1,6 @@
+
+user.name is {user.name}
+
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index 2114eb7656ac..bfc5e20aaeea 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -1992,4 +1992,14 @@ test.describe.parallel('XSS', () => {
// @ts-expect-error - check global injected variable
expect(await page.evaluate(() => window.pwned)).toBeUndefined();
});
+
+ test('no xss via shadow endpoint', async ({ page }) => {
+ await page.goto('/xss/shadow');
+
+ // @ts-expect-error - check global injected variable
+ expect(await page.evaluate(() => window.pwned)).toBeUndefined();
+ expect(await page.textContent('h1')).toBe(
+ 'user.name is '
+ );
+ });
});