fix: escape more template-literal-related characters (#13262)

* fix: escape more template-literal-related characters

Escape `{` at the start of a string, because it could be preceeded by a `$`, which in combination loads to the following characters being treated as a value

Fixes #13258

(used the opportunity to merge closely-related tests into one)

* sanitize template strings once assembled (#13263)

* only sanitize template quasis once assembled

* changeset

* remove old changeset

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/13257/head
Simon H 3 months ago committed by GitHub
parent d9369d8e30
commit 9864138022
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: wait until template strings are complete before sanitizing

@ -54,10 +54,10 @@ export function build_template_literal(values, visit, state) {
const node = values[i];
if (node.type === 'Text') {
quasi.value.raw += sanitize_template_string(node.data);
quasi.value.cooked += node.data;
} else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') {
if (node.expression.value != null) {
quasi.value.raw += sanitize_template_string(node.expression.value + '');
quasi.value.cooked += node.expression.value + '';
}
} else {
if (contains_multiple_call_expression) {
@ -91,6 +91,10 @@ export function build_template_literal(values, visit, state) {
}
}
for (const quasi of quasis) {
quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
}
const value = b.template(quasis, expressions);
return { value, has_state, has_call };

@ -42,12 +42,11 @@ export function process_children(nodes, { visit, state }) {
const node = sequence[i];
if (node.type === 'Text' || node.type === 'Comment') {
quasi.value.raw += sanitize_template_string(
node.type === 'Comment' ? `<!--${node.data}-->` : escape_html(node.data)
);
quasi.value.cooked +=
node.type === 'Comment' ? `<!--${node.data}-->` : escape_html(node.data);
} else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') {
if (node.expression.value != null) {
quasi.value.raw += sanitize_template_string(escape_html(node.expression.value + ''));
quasi.value.cooked += escape_html(node.expression.value + '');
}
} else {
expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression))));
@ -57,6 +56,10 @@ export function process_children(nodes, { visit, state }) {
}
}
for (const quasi of quasis) {
quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
}
state.template.push(b.template(quasis, expressions));
}
@ -95,8 +98,8 @@ function is_statement(node) {
* @returns {Statement[]}
*/
export function build_template(template, out = b.id('$$payload.out'), operator = '+=') {
/** @type {TemplateElement[]} */
let quasis = [];
/** @type {string[]} */
let strings = [];
/** @type {Expression[]} */
let expressions = [];
@ -105,8 +108,19 @@ export function build_template(template, out = b.id('$$payload.out'), operator =
const statements = [];
const flush = () => {
statements.push(b.stmt(b.assignment(operator, out, b.template(quasis, expressions))));
quasis = [];
statements.push(
b.stmt(
b.assignment(
operator,
out,
b.template(
strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)),
expressions
)
)
)
);
strings = [];
expressions = [];
};
@ -114,30 +128,30 @@ export function build_template(template, out = b.id('$$payload.out'), operator =
const node = template[i];
if (is_statement(node)) {
if (quasis.length !== 0) {
if (strings.length !== 0) {
flush();
}
statements.push(node);
} else {
let last = quasis.at(-1);
if (!last) quasis.push((last = b.quasi('', false)));
if (strings.length === 0) {
strings.push('');
}
if (node.type === 'Literal') {
last.value.raw +=
typeof node.value === 'string' ? sanitize_template_string(node.value) : node.value;
strings[strings.length - 1] += node.value;
} else if (node.type === 'TemplateLiteral') {
last.value.raw += node.quasis[0].value.raw;
quasis.push(...node.quasis.slice(1));
strings[strings.length - 1] += node.quasis[0].value.cooked;
strings.push(...node.quasis.slice(1).map((q) => /** @type {string} */ (q.value.cooked)));
expressions.push(...node.expressions);
} else {
expressions.push(node);
quasis.push(b.quasi('', i + 1 === template.length || is_statement(template[i + 1])));
strings.push('');
}
}
}
if (quasis.length !== 0) {
if (strings.length !== 0) {
flush();
}

@ -1,5 +1,8 @@
import { test } from '../../test';
export default test({
html: '<code>`${foo}\\n`</code>\n`\n<div title="`${foo}\\n`">foo</div>\n<div>`${foo}\\n`</div>'
html:
'<code>`${foo}\\n`</code>\n`\n<div title="`${foo}\\n`">foo</div>\n<div>`${foo}\\n`</div>' +
'<div>/ $clicks: 0 `tim$es` \\</div><div>$dollars `backticks` pyramid /\\</div>' +
'<p>${ ${ ${</p>'
});

@ -6,3 +6,14 @@
{@html "`"}
<div title="`$&#123;foo}\n`">foo</div>
<Widget value="`$&#123;foo}\n`"/>
<div>
/ $clicks: {0} `tim${"e"}s` \
</div>
<div>
$dollars `backticks` pyramid /\
</div>
<p>
${'{'}
&dollar;{'{'}
{'$'}{'{'}
</p>

@ -1,5 +0,0 @@
import { test } from '../../test';
export default test({
html: '<div>/ $clicks: 0 `tim$es` \\</div><div>$dollars `backticks` pyramid /\\</div>'
});

@ -1,6 +0,0 @@
<div>
/ $clicks: {0} `tim${"e"}s` \
</div>
<div>
$dollars `backticks` pyramid /\
</div>
Loading…
Cancel
Save