Skip to content

fixes shadow hydration escaping #3793

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 9, 2022
5 changes: 5 additions & 0 deletions .changeset/late-foxes-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fixes shadow hydration escaping
4 changes: 2 additions & 2 deletions packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -261,7 +261,7 @@ export async function render_response({

if (shadow_props) {
// prettier-ignore
body += `<script type="application/json" data-type="svelte-props">${s(shadow_props)}</script>`;
body += `<script type="application/json" data-type="svelte-props">${escape_json_in_html(s(shadow_props))}</script>`;
}
}

Expand Down
36 changes: 33 additions & 3 deletions packages/kit/src/utils/escape.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/** @type {Record<string, string>} */
const escape_json_string_in_html_dict = {
'"': '\\"',
const escape_json_in_html_dict = {
'<': '\\u003C',
'>': '\\u003E',
'/': '\\u002F',
Expand All @@ -15,7 +14,38 @@ const escape_json_string_in_html_dict = {
'\u2029': '\\u2029'
};

/** @param {string} str */
/** @type {Record<string, string>} */
const escape_json_string_in_html_dict = {
'"': '\\"',
...escape_json_in_html_dict
};

/**
* escape a json string to be embedded into a script data tag
*
* <script>
* output here
* </script>
* @param {string} str
*/
export function escape_json_in_html(str) {
return escape(
str,
escape_json_in_html_dict,
(code) => `\\u${code.toString(16).toUpperCase()}`
);
}

/**
* escape a json string to be embedded into a larger json object thats going to be embedded in html
*
* <script>
* {
* "foo":"output here"
* }
* </script>
* @param {string} str
*/
export function escape_json_string_in_html(str) {
return escape(
str,
Expand Down
8 changes: 8 additions & 0 deletions packages/kit/test/apps/basics/src/routes/xss/shadow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('@sveltejs/kit').RequestHandler} */
export function get() {
return {
body: {
pwned: '</script><script>window.pwned = 1</script>'
}
};
}
5 changes: 5 additions & 0 deletions packages/kit/test/apps/basics/src/routes/xss/shadow.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
export let pwned;
</script>
<h1>failed script inject is: {pwned}</h1>

10 changes: 10 additions & 0 deletions packages/kit/test/apps/basics/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
'failed script inject is: </script><script>window.pwned = 1</script>'
);
});
});