Skip to content

Commit 59badb5

Browse files
authored
[fix] escape data-url attribute in serialized SSR response (#2534)
1 parent 24d7b95 commit 59badb5

File tree

5 files changed

+90
-50
lines changed

5 files changed

+90
-50
lines changed

.changeset/cyan-knives-mix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
Fix escaping of URLs of endpoint responses serialized into SSR response

packages/kit/src/runtime/client/renderer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ function page_store(value) {
4747
function initial_fetch(resource, opts) {
4848
const url = typeof resource === 'string' ? resource : resource.url;
4949

50-
let selector = `script[data-type="svelte-data"][data-url="${url}"]`;
50+
let selector = `script[data-type="svelte-data"][data-url=${JSON.stringify(url)}]`;
5151

5252
if (opts && typeof opts.body === 'string') {
5353
selector += `[data-body="${hash(opts.body)}"]`;

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

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { normalize } from '../../load.js';
22
import { respond } from '../index.js';
3+
import { escape_json_string_in_html } from '../../../utils/escape.js';
34

45
const s = JSON.stringify;
56

@@ -236,7 +237,7 @@ export async function load_node({
236237
fetched.push({
237238
url,
238239
body: /** @type {string} */ (opts.body),
239-
json: `{"status":${response.status},"statusText":${s(response.statusText)},"headers":${s(headers)},"body":${escape(body)}}`
240+
json: `{"status":${response.status},"statusText":${s(response.statusText)},"headers":${s(headers)},"body":"${escape_json_string_in_html(body)}"}`
240241
});
241242
}
242243

@@ -300,53 +301,6 @@ export async function load_node({
300301
};
301302
}
302303

303-
/** @type {Record<string, string>} */
304-
const escaped = {
305-
'<': '\\u003C',
306-
'>': '\\u003E',
307-
'/': '\\u002F',
308-
'\\': '\\\\',
309-
'\b': '\\b',
310-
'\f': '\\f',
311-
'\n': '\\n',
312-
'\r': '\\r',
313-
'\t': '\\t',
314-
'\0': '\\0',
315-
'\u2028': '\\u2028',
316-
'\u2029': '\\u2029'
317-
};
318-
319-
/** @param {string} str */
320-
function escape(str) {
321-
let result = '"';
322-
323-
for (let i = 0; i < str.length; i += 1) {
324-
const char = str.charAt(i);
325-
const code = char.charCodeAt(0);
326-
327-
if (char === '"') {
328-
result += '\\"';
329-
} else if (char in escaped) {
330-
result += escaped[char];
331-
} else if (code >= 0xd800 && code <= 0xdfff) {
332-
const next = str.charCodeAt(i + 1);
333-
334-
// If this is the beginning of a [high, low] surrogate pair,
335-
// add the next two characters, otherwise escape
336-
if (code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
337-
result += char + str[++i];
338-
} else {
339-
result += `\\u${code.toString(16).toUpperCase()}`;
340-
}
341-
} else {
342-
result += char;
343-
}
344-
}
345-
346-
result += '"';
347-
return result;
348-
}
349-
350304
const absolute = /^([a-z]+:)?\/?\//;
351305

352306
/**

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import devalue from 'devalue';
22
import { writable } from 'svelte/store';
33
import { coalesce_to_error } from '../../../utils/error.js';
44
import { hash } from '../../hash.js';
5+
import { escape_html_attr } from '../../../utils/escape.js';
56

67
const s = JSON.stringify;
78

@@ -168,7 +169,9 @@ export async function render_response({
168169
169170
${serialized_data
170171
.map(({ url, body, json }) => {
171-
let attributes = `type="application/json" data-type="svelte-data" data-url="${url}"`;
172+
let attributes = `type="application/json" data-type="svelte-data" data-url=${escape_html_attr(
173+
url
174+
)}`;
172175
if (body) attributes += ` data-body="${hash(body)}"`;
173176
174177
return `<script ${attributes}>${json}</script>`;

packages/kit/src/utils/escape.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/** @type {Record<string, string>} */
2+
const escape_json_string_in_html_dict = {
3+
'"': '\\"',
4+
'<': '\\u003C',
5+
'>': '\\u003E',
6+
'/': '\\u002F',
7+
'\\': '\\\\',
8+
'\b': '\\b',
9+
'\f': '\\f',
10+
'\n': '\\n',
11+
'\r': '\\r',
12+
'\t': '\\t',
13+
'\0': '\\0',
14+
'\u2028': '\\u2028',
15+
'\u2029': '\\u2029'
16+
};
17+
18+
/** @param {string} str */
19+
export function escape_json_string_in_html(str) {
20+
return escape(
21+
str,
22+
escape_json_string_in_html_dict,
23+
(code) => `\\u${code.toString(16).toUpperCase()}`
24+
);
25+
}
26+
27+
/** @type {Record<string, string>} */
28+
const escape_html_attr_dict = {
29+
'<': '&lt;',
30+
'>': '&gt;',
31+
'"': '&quot;'
32+
};
33+
34+
/**
35+
* use for escaping string values to be used html attributes on the page
36+
* e.g.
37+
* <script data-url="here">
38+
*
39+
* @param {string} str
40+
* @returns string escaped string
41+
*/
42+
export function escape_html_attr(str) {
43+
return '"' + escape(str, escape_html_attr_dict, (code) => `&#${code};`) + '"';
44+
}
45+
46+
/**
47+
*
48+
* @param str {string} string to escape
49+
* @param dict {Record<string, string>} dictionary of character replacements
50+
* @param unicode_encoder {function(number): string} encoder to use for high unicode characters
51+
* @returns {string}
52+
*/
53+
function escape(str, dict, unicode_encoder) {
54+
let result = '';
55+
56+
for (let i = 0; i < str.length; i += 1) {
57+
const char = str.charAt(i);
58+
const code = char.charCodeAt(0);
59+
60+
if (char in dict) {
61+
result += dict[char];
62+
} else if (code >= 0xd800 && code <= 0xdfff) {
63+
const next = str.charCodeAt(i + 1);
64+
65+
// If this is the beginning of a [high, low] surrogate pair,
66+
// add the next two characters, otherwise escape
67+
if (code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
68+
result += char + str[++i];
69+
} else {
70+
result += unicode_encoder(code);
71+
}
72+
} else {
73+
result += char;
74+
}
75+
}
76+
77+
return result;
78+
}

0 commit comments

Comments
 (0)