Skip to content

Commit

Permalink
[fix] escape data-url attribute in serialized SSR response (#2534)
Browse files Browse the repository at this point in the history
  • Loading branch information
dominikg authored Oct 1, 2021
1 parent 24d7b95 commit 59badb5
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 50 deletions.
5 changes: 5 additions & 0 deletions .changeset/cyan-knives-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Fix escaping of URLs of endpoint responses serialized into SSR response
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/client/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function page_store(value) {
function initial_fetch(resource, opts) {
const url = typeof resource === 'string' ? resource : resource.url;

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

if (opts && typeof opts.body === 'string') {
selector += `[data-body="${hash(opts.body)}"]`;
Expand Down
50 changes: 2 additions & 48 deletions packages/kit/src/runtime/server/page/load_node.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { normalize } from '../../load.js';
import { respond } from '../index.js';
import { escape_json_string_in_html } from '../../../utils/escape.js';

const s = JSON.stringify;

Expand Down Expand Up @@ -236,7 +237,7 @@ export async function load_node({
fetched.push({
url,
body: /** @type {string} */ (opts.body),
json: `{"status":${response.status},"statusText":${s(response.statusText)},"headers":${s(headers)},"body":${escape(body)}}`
json: `{"status":${response.status},"statusText":${s(response.statusText)},"headers":${s(headers)},"body":"${escape_json_string_in_html(body)}"}`
});
}

Expand Down Expand Up @@ -300,53 +301,6 @@ export async function load_node({
};
}

/** @type {Record<string, string>} */
const escaped = {
'<': '\\u003C',
'>': '\\u003E',
'/': '\\u002F',
'\\': '\\\\',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'\0': '\\0',
'\u2028': '\\u2028',
'\u2029': '\\u2029'
};

/** @param {string} str */
function escape(str) {
let result = '"';

for (let i = 0; i < str.length; i += 1) {
const char = str.charAt(i);
const code = char.charCodeAt(0);

if (char === '"') {
result += '\\"';
} else if (char in escaped) {
result += escaped[char];
} else if (code >= 0xd800 && code <= 0xdfff) {
const next = str.charCodeAt(i + 1);

// If this is the beginning of a [high, low] surrogate pair,
// add the next two characters, otherwise escape
if (code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
result += char + str[++i];
} else {
result += `\\u${code.toString(16).toUpperCase()}`;
}
} else {
result += char;
}
}

result += '"';
return result;
}

const absolute = /^([a-z]+:)?\/?\//;

/**
Expand Down
5 changes: 4 additions & 1 deletion packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import devalue from 'devalue';
import { 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';

const s = JSON.stringify;

Expand Down Expand Up @@ -168,7 +169,9 @@ export async function render_response({
${serialized_data
.map(({ url, body, json }) => {
let attributes = `type="application/json" data-type="svelte-data" data-url="${url}"`;
let attributes = `type="application/json" data-type="svelte-data" data-url=${escape_html_attr(
url
)}`;
if (body) attributes += ` data-body="${hash(body)}"`;
return `<script ${attributes}>${json}</script>`;
Expand Down
78 changes: 78 additions & 0 deletions packages/kit/src/utils/escape.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/** @type {Record<string, string>} */
const escape_json_string_in_html_dict = {
'"': '\\"',
'<': '\\u003C',
'>': '\\u003E',
'/': '\\u002F',
'\\': '\\\\',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'\0': '\\0',
'\u2028': '\\u2028',
'\u2029': '\\u2029'
};

/** @param {string} str */
export function escape_json_string_in_html(str) {
return escape(
str,
escape_json_string_in_html_dict,
(code) => `\\u${code.toString(16).toUpperCase()}`
);
}

/** @type {Record<string, string>} */
const escape_html_attr_dict = {
'<': '&lt;',
'>': '&gt;',
'"': '&quot;'
};

/**
* use for escaping string values to be used html attributes on the page
* e.g.
* <script data-url="here">
*
* @param {string} str
* @returns string escaped string
*/
export function escape_html_attr(str) {
return '"' + escape(str, escape_html_attr_dict, (code) => `&#${code};`) + '"';
}

/**
*
* @param str {string} string to escape
* @param dict {Record<string, string>} dictionary of character replacements
* @param unicode_encoder {function(number): string} encoder to use for high unicode characters
* @returns {string}
*/
function escape(str, dict, unicode_encoder) {
let result = '';

for (let i = 0; i < str.length; i += 1) {
const char = str.charAt(i);
const code = char.charCodeAt(0);

if (char in dict) {
result += dict[char];
} else if (code >= 0xd800 && code <= 0xdfff) {
const next = str.charCodeAt(i + 1);

// If this is the beginning of a [high, low] surrogate pair,
// add the next two characters, otherwise escape
if (code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
result += char + str[++i];
} else {
result += unicode_encoder(code);
}
} else {
result += char;
}
}

return result;
}

0 comments on commit 59badb5

Please sign in to comment.