Skip to content

Commit 8c8e4e6

Browse files
committed
fix: use separate escape util that does not escape double quotes; add testcase
1 parent a3c2316 commit 8c8e4e6

File tree

5 files changed

+58
-5
lines changed

5 files changed

+58
-5
lines changed

packages/kit/src/runtime/server/page/render.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import devalue from 'devalue';
22
import { readable, writable } from 'svelte/store';
33
import { coalesce_to_error } from '../../../utils/error.js';
44
import { hash } from '../../hash.js';
5-
import { escape_html_attr, escape_json_string_in_html } from '../../../utils/escape.js';
5+
import { escape_html_attr, escape_json_in_html } from '../../../utils/escape.js';
66
import { s } from '../../../utils/misc.js';
77
import { create_prerendering_url_proxy } from './utils.js';
88
import { Csp, csp_ready } from './csp.js';
@@ -261,7 +261,7 @@ export async function render_response({
261261

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

packages/kit/src/utils/escape.js

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/** @type {Record<string, string>} */
2-
const escape_json_string_in_html_dict = {
3-
'"': '\\"',
2+
const escape_json_in_html_dict = {
43
'<': '\\u003C',
54
'>': '\\u003E',
65
'/': '\\u002F',
@@ -15,7 +14,38 @@ const escape_json_string_in_html_dict = {
1514
'\u2029': '\\u2029'
1615
};
1716

18-
/** @param {string} str */
17+
/** @type {Record<string, string>} */
18+
const escape_json_string_in_html_dict = {
19+
'"': '\\"',
20+
...escape_json_in_html_dict
21+
};
22+
23+
/**
24+
* escape a json string to be embedded into a script data tag
25+
*
26+
* <script>
27+
* output here
28+
* </script>
29+
* @param {string} str
30+
*/
31+
export function escape_json_in_html(str) {
32+
return escape(
33+
str,
34+
escape_json_in_html_dict,
35+
(code) => `\\u${code.toString(16).toUpperCase()}`
36+
);
37+
}
38+
39+
/**
40+
* escape a json string to be embedded into a larger json object thats going to be embedded in html
41+
*
42+
* <script>
43+
* {
44+
* "foo":"output here"
45+
* }
46+
* </script>
47+
* @param {string} str
48+
*/
1949
export function escape_json_string_in_html(str) {
2050
return escape(
2151
str,
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/** @type {import('@sveltejs/kit').RequestHandler} */
2+
export function get() {
3+
return {
4+
body: {
5+
pwned: '</script><script>window.pwned = 1</script>'
6+
}
7+
};
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
export let pwned;
3+
</script>
4+
<h1>failed script inject is: {pwned}</h1>
5+

packages/kit/test/apps/basics/test/test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1992,4 +1992,14 @@ test.describe.parallel('XSS', () => {
19921992
// @ts-expect-error - check global injected variable
19931993
expect(await page.evaluate(() => window.pwned)).toBeUndefined();
19941994
});
1995+
1996+
test('no xss via shadow endpoint', async ({ page }) => {
1997+
await page.goto('/xss/shadow')
1998+
1999+
// @ts-expect-error - check global injected variable
2000+
expect(await page.evaluate(() => window.pwned)).toBeUndefined();
2001+
expect(await page.textContent('h1')).toBe(
2002+
'failed script inject is: </script><script>window.pwned = 1</script>'
2003+
);
2004+
});
19952005
});

0 commit comments

Comments
 (0)