diff --git a/.changeset/blue-dragons-see.md b/.changeset/blue-dragons-see.md new file mode 100644 index 0000000000..fb412ce4ed --- /dev/null +++ b/.changeset/blue-dragons-see.md @@ -0,0 +1,14 @@ +--- +"marko": patch +"@marko/runtime-tags": patch +--- + +Fix escaping issue for dynamic text interpolation inside `"} +``` + +Note that `script` and `style` there should _never_ render unsanitized user defined values, regardless of wether or not the closing tag is escaped, since these are conceptually just "eval". diff --git a/.changeset/frank-memes-knock.md b/.changeset/frank-memes-knock.md new file mode 100644 index 0000000000..25e56dc10d --- /dev/null +++ b/.changeset/frank-memes-knock.md @@ -0,0 +1,13 @@ +--- +"marko": patch +"@marko/runtime-tags": patch +--- + +Fix escaping for `` tag. +Previously this tag relied on normal xml escaping which looks for `<`. +This PR updates to have a special escape for `` tags that replaces `>` instead. + +```marko +// Previously incorrectly escaped. +${">Uh oh"} +``` diff --git a/packages/runtime-class/src/runtime/html/helpers/escape-comment-placeholder.js b/packages/runtime-class/src/runtime/html/helpers/escape-comment-placeholder.js new file mode 100644 index 0000000000..350a007a0a --- /dev/null +++ b/packages/runtime-class/src/runtime/html/helpers/escape-comment-placeholder.js @@ -0,0 +1,17 @@ +"use strict"; +const unsafeCharsReg = />/g; +const replaceMatch = () => ">"; +const escape = (str) => + unsafeCharsReg.test(str) ? str.replace(unsafeCharsReg, replaceMatch) : str; + +/** + * Escapes content placed inside an tag. + * + * For example: + * ${userInput} + * + * Without escaping, a value of `>`, - ]); + const { file } = path.hub; + const nodes = [writeHTML``); + path.replaceWithMultiple(nodes.filter(Boolean)); } else { const templateQuasis = []; const templateExpressions = []; diff --git a/packages/runtime-class/test/render/fixtures/escape-comment/expected.html b/packages/runtime-class/test/render/fixtures/escape-comment/expected.html new file mode 100644 index 0000000000..90eb036ed8 --- /dev/null +++ b/packages/runtime-class/test/render/fixtures/escape-comment/expected.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/runtime-class/test/render/fixtures/escape-comment/template.marko b/packages/runtime-class/test/render/fixtures/escape-comment/template.marko new file mode 100644 index 0000000000..019e855e8a --- /dev/null +++ b/packages/runtime-class/test/render/fixtures/escape-comment/template.marko @@ -0,0 +1 @@ +${input.text} diff --git a/packages/runtime-class/test/render/fixtures/escape-comment/test.js b/packages/runtime-class/test/render/fixtures/escape-comment/test.js new file mode 100644 index 0000000000..eb04215fef --- /dev/null +++ b/packages/runtime-class/test/render/fixtures/escape-comment/test.js @@ -0,0 +1,3 @@ +exports.templateData = { + text: "-->", +}; diff --git a/packages/runtime-class/test/render/fixtures/escape-comment/vdom-expected.html b/packages/runtime-class/test/render/fixtures/escape-comment/vdom-expected.html new file mode 100644 index 0000000000..b2c6a1f288 --- /dev/null +++ b/packages/runtime-class/test/render/fixtures/escape-comment/vdom-expected.html @@ -0,0 +1 @@ +"--> diff --git a/packages/runtime-class/test/render/fixtures/escape-script-case/expected.html b/packages/runtime-class/test/render/fixtures/escape-script-case/expected.html new file mode 100644 index 0000000000..50ec8d57c7 --- /dev/null +++ b/packages/runtime-class/test/render/fixtures/escape-script-case/expected.html @@ -0,0 +1,3 @@ +
{"name":"Evil </SCRIPT>"}
\ No newline at end of file diff --git a/packages/runtime-class/test/render/fixtures/escape-script-case/template.marko b/packages/runtime-class/test/render/fixtures/escape-script-case/template.marko new file mode 100644 index 0000000000..4ff4986f79 --- /dev/null +++ b/packages/runtime-class/test/render/fixtures/escape-script-case/template.marko @@ -0,0 +1,4 @@ + +
${JSON.stringify(input.foo)}
\ No newline at end of file diff --git a/packages/runtime-class/test/render/fixtures/escape-script-case/test.js b/packages/runtime-class/test/render/fixtures/escape-script-case/test.js new file mode 100644 index 0000000000..c0a15e5cc7 --- /dev/null +++ b/packages/runtime-class/test/render/fixtures/escape-script-case/test.js @@ -0,0 +1,5 @@ +exports.templateData = { + foo: { + name: "Evil ", + }, +}; diff --git a/packages/runtime-class/test/render/fixtures/escape-script-case/vdom-expected.html b/packages/runtime-class/test/render/fixtures/escape-script-case/vdom-expected.html new file mode 100644 index 0000000000..591eb6cbd5 --- /dev/null +++ b/packages/runtime-class/test/render/fixtures/escape-script-case/vdom-expected.html @@ -0,0 +1,4 @@ +\"};\n" +
+  "{\"name\":\"Evil \"}"
diff --git a/packages/runtime-class/test/render/fixtures/escape-style-case/expected.html b/packages/runtime-class/test/render/fixtures/escape-style-case/expected.html
new file mode 100644
index 0000000000..067d80802e
--- /dev/null
+++ b/packages/runtime-class/test/render/fixtures/escape-style-case/expected.html
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/packages/runtime-class/test/render/fixtures/escape-style-case/template.marko b/packages/runtime-class/test/render/fixtures/escape-style-case/template.marko
new file mode 100644
index 0000000000..37c1ff9d51
--- /dev/null
+++ b/packages/runtime-class/test/render/fixtures/escape-style-case/template.marko
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/packages/runtime-class/test/render/fixtures/escape-style-case/test.js b/packages/runtime-class/test/render/fixtures/escape-style-case/test.js
new file mode 100644
index 0000000000..49ffcd626c
--- /dev/null
+++ b/packages/runtime-class/test/render/fixtures/escape-style-case/test.js
@@ -0,0 +1,3 @@
+exports.templateData = {
+  color: '',
+};
diff --git a/packages/runtime-class/test/render/fixtures/escape-style-case/vdom-expected.html b/packages/runtime-class/test/render/fixtures/escape-style-case/vdom-expected.html
new file mode 100644
index 0000000000..e44cc7224b
--- /dev/null
+++ b/packages/runtime-class/test/render/fixtures/escape-style-case/vdom-expected.html
@@ -0,0 +1,2 @@
+;\n    }\n"
diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-comment-counter/__snapshots__/html.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/html-comment-counter/__snapshots__/html.expected/template.js
index 37d8b6e5c1..2c14854fa5 100644
--- a/packages/runtime-tags/src/__tests__/fixtures/html-comment-counter/__snapshots__/html.expected/template.js
+++ b/packages/runtime-tags/src/__tests__/fixtures/html-comment-counter/__snapshots__/html.expected/template.js
@@ -3,7 +3,7 @@ export default _._template("__tests__/template.marko", input => {
   _._scope_reason();
   const $scope0_id = _._scope_id();
   let count = 0;
-  _._html(`
${_._el_resume($scope0_id, "#button/0")}${_._el_resume($scope0_id, "#comment/2")}
`); + _._html(`
${_._el_resume($scope0_id, "#button/0")}${_._el_resume($scope0_id, "#comment/2")}
`); _._script($scope0_id, "__tests__/template.marko_0_count"); _._scope($scope0_id, { count diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/.name-cache.json new file mode 100644 index 0000000000..db413faf9a --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/.name-cache.json @@ -0,0 +1,5 @@ +{ + "vars": { + "props": {} + } +} diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/csr-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/csr-sanitized.expected.md new file mode 100644 index 0000000000..ba2aa26adb --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/csr-sanitized.expected.md @@ -0,0 +1 @@ +# Render diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/csr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/csr.expected.md new file mode 100644 index 0000000000..586d7abdd9 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/csr.expected.md @@ -0,0 +1,9 @@ +# Render +```html + +``` + +# Mutations +``` +INSERT #comment +``` \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/dom.expected/template.hydrate.js b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/dom.expected/template.hydrate.js new file mode 100644 index 0000000000..3eb9cd79d3 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/dom.expected/template.hydrate.js @@ -0,0 +1 @@ +// size: 0 diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/dom.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/dom.expected/template.js new file mode 100644 index 0000000000..f3f0c0e75b --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/dom.expected/template.js @@ -0,0 +1,7 @@ +export const $template = ""; +export const $walks = /* get, over(1) */" b"; +import * as _ from "@marko/runtime-tags/debug/dom"; +export function $setup($scope) { + _._text($scope["#comment/0"], `${_._to_text("-->")}`); +} +export default /* @__PURE__ */_._template("__tests__/template.marko", $template, $walks, $setup); \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/html.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/html.expected/template.js new file mode 100644 index 0000000000..bfbbc4d336 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/html.expected/template.js @@ -0,0 +1,6 @@ +import * as _ from "@marko/runtime-tags/debug/html"; +export default _._template("__tests__/template.marko", input => { + _._scope_reason(); + const $scope0_id = _._scope_id(); + _._html(`")}-->`); +}, 1); \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/resume-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/resume-sanitized.expected.md new file mode 100644 index 0000000000..ba2aa26adb --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/resume-sanitized.expected.md @@ -0,0 +1 @@ +# Render diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/resume.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/resume.expected.md new file mode 100644 index 0000000000..365737353e --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/resume.expected.md @@ -0,0 +1,8 @@ +# Render +```html + + + + + +``` diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/ssr-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/ssr-sanitized.expected.md new file mode 100644 index 0000000000..ef85d03440 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/ssr-sanitized.expected.md @@ -0,0 +1 @@ +# Render End diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/ssr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/ssr.expected.md new file mode 100644 index 0000000000..448176687d --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/__snapshots__/ssr.expected.md @@ -0,0 +1,21 @@ +# Write +```html + +``` + +# Render End +```html + + + + + +``` + +# Mutations +``` +INSERT #comment +INSERT html +INSERT html/head +INSERT html/body +``` \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/template.marko b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/template.marko new file mode 100644 index 0000000000..aba548c30c --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/template.marko @@ -0,0 +1 @@ +${"-->"} diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/test.ts b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/test.ts new file mode 100644 index 0000000000..53375135d8 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-comment-placeholder/test.ts @@ -0,0 +1,5 @@ +import type { TestConfig } from "../../main.test"; + +export const config: TestConfig = { + steps: [{}], +}; diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/.name-cache.json new file mode 100644 index 0000000000..db413faf9a --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/.name-cache.json @@ -0,0 +1,5 @@ +{ + "vars": { + "props": {} + } +} diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/csr-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/csr-sanitized.expected.md new file mode 100644 index 0000000000..ba2aa26adb --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/csr-sanitized.expected.md @@ -0,0 +1 @@ +# Render diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/csr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/csr.expected.md new file mode 100644 index 0000000000..71efbf4060 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/csr.expected.md @@ -0,0 +1,11 @@ +# Render +```html + +``` + +# Mutations +``` +INSERT script +``` \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/dom.expected/template.hydrate.js b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/dom.expected/template.hydrate.js new file mode 100644 index 0000000000..3eb9cd79d3 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/dom.expected/template.hydrate.js @@ -0,0 +1 @@ +// size: 0 diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/dom.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/dom.expected/template.js new file mode 100644 index 0000000000..90a9a02e3d --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/dom.expected/template.js @@ -0,0 +1,9 @@ +export const $template = ""; +export const $walks = /* get, over(1) */" b"; +import * as _ from "@marko/runtime-tags/debug/dom"; +const $injection = /* @__PURE__ */_._let("injection/1", $scope => _._text_content($scope["#script/0"], `var x = '${_._to_text($scope.injection)}'`)); +export function $setup($scope) { + _._attr_nonce($scope, "#script/0"); + $injection($scope, ""); +} +export default /* @__PURE__ */_._template("__tests__/template.marko", $template, $walks, $setup); \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/html.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/html.expected/template.js new file mode 100644 index 0000000000..b29927ad38 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/html.expected/template.js @@ -0,0 +1,8 @@ +import * as _ from "@marko/runtime-tags/debug/html"; +export default _._template("__tests__/template.marko", input => { + _._scope_reason(); + const $scope0_id = _._scope_id(); + let injection = ""; + _._html(`var x = '${_._escape_script(injection)}'`); + _._resume_branch($scope0_id); +}, 1); \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/resume-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/resume-sanitized.expected.md new file mode 100644 index 0000000000..ba2aa26adb --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/resume-sanitized.expected.md @@ -0,0 +1 @@ +# Render diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/resume.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/resume.expected.md new file mode 100644 index 0000000000..02ab76f066 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/resume.expected.md @@ -0,0 +1,11 @@ +# Render +```html + + + + + + +``` diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/ssr-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/ssr-sanitized.expected.md new file mode 100644 index 0000000000..ef85d03440 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/ssr-sanitized.expected.md @@ -0,0 +1 @@ +# Render End diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/ssr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/ssr.expected.md new file mode 100644 index 0000000000..1e305281e0 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/__snapshots__/ssr.expected.md @@ -0,0 +1,25 @@ +# Write +```html + +``` + +# Render End +```html + + + + + + +``` + +# Mutations +``` +INSERT html +INSERT html/head +INSERT html/head/script +INSERT html/head/script/#text +INSERT html/body +``` \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/template.marko b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/template.marko new file mode 100644 index 0000000000..146d01c3bf --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/template.marko @@ -0,0 +1,2 @@ + +var x = '${injection}' diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/test.ts b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/test.ts new file mode 100644 index 0000000000..53375135d8 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-script-injection/test.ts @@ -0,0 +1,5 @@ +import type { TestConfig } from "../../main.test"; + +export const config: TestConfig = { + steps: [{}], +}; diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/.name-cache.json new file mode 100644 index 0000000000..db413faf9a --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/.name-cache.json @@ -0,0 +1,5 @@ +{ + "vars": { + "props": {} + } +} diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/csr-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/csr-sanitized.expected.md new file mode 100644 index 0000000000..ba2aa26adb --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/csr-sanitized.expected.md @@ -0,0 +1 @@ +# Render diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/csr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/csr.expected.md new file mode 100644 index 0000000000..de9a3a6755 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/csr.expected.md @@ -0,0 +1,11 @@ +# Render +```html + +``` + +# Mutations +``` +INSERT style +``` \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/dom.expected/template.hydrate.js b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/dom.expected/template.hydrate.js new file mode 100644 index 0000000000..3eb9cd79d3 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/dom.expected/template.hydrate.js @@ -0,0 +1 @@ +// size: 0 diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/dom.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/dom.expected/template.js new file mode 100644 index 0000000000..85494672a0 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/dom.expected/template.js @@ -0,0 +1,9 @@ +export const $template = ""; +export const $walks = /* get, over(1) */" b"; +import * as _ from "@marko/runtime-tags/debug/dom"; +const $injection = /* @__PURE__ */_._let("injection/1", $scope => _._text_content($scope["#style/0"], `.evil { content: '${_._to_text($scope.injection)}'; }`)); +export function $setup($scope) { + _._attr_nonce($scope, "#style/0"); + $injection($scope, ""); +} +export default /* @__PURE__ */_._template("__tests__/template.marko", $template, $walks, $setup); \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/html.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/html.expected/template.js new file mode 100644 index 0000000000..ced724a199 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/html.expected/template.js @@ -0,0 +1,8 @@ +import * as _ from "@marko/runtime-tags/debug/html"; +export default _._template("__tests__/template.marko", input => { + _._scope_reason(); + const $scope0_id = _._scope_id(); + let injection = ""; + _._html(`.evil { content: '${_._escape_style(injection)}'; }`); + _._resume_branch($scope0_id); +}, 1); \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/resume-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/resume-sanitized.expected.md new file mode 100644 index 0000000000..ba2aa26adb --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/resume-sanitized.expected.md @@ -0,0 +1 @@ +# Render diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/resume.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/resume.expected.md new file mode 100644 index 0000000000..0307208aa7 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/resume.expected.md @@ -0,0 +1,11 @@ +# Render +```html + + + + + + +``` diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/ssr-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/ssr-sanitized.expected.md new file mode 100644 index 0000000000..ef85d03440 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/ssr-sanitized.expected.md @@ -0,0 +1 @@ +# Render End diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/ssr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/ssr.expected.md new file mode 100644 index 0000000000..69b0232e3a --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/__snapshots__/ssr.expected.md @@ -0,0 +1,25 @@ +# Write +```html + +``` + +# Render End +```html + + + + + + +``` + +# Mutations +``` +INSERT html +INSERT html/head +INSERT html/head/style +INSERT html/head/style/#text +INSERT html/body +``` \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/template.marko b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/template.marko new file mode 100644 index 0000000000..5d479756a2 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/template.marko @@ -0,0 +1,2 @@ + +.evil { content: '${injection}'; } diff --git a/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/test.ts b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/test.ts new file mode 100644 index 0000000000..53375135d8 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/html-style-injection/test.ts @@ -0,0 +1,5 @@ +import type { TestConfig } from "../../main.test"; + +export const config: TestConfig = { + steps: [{}], +}; diff --git a/packages/runtime-tags/src/__tests__/html-content.test.ts b/packages/runtime-tags/src/__tests__/html-content.test.ts index 16378c9609..d0d410dd5c 100644 --- a/packages/runtime-tags/src/__tests__/html-content.test.ts +++ b/packages/runtime-tags/src/__tests__/html-content.test.ts @@ -72,6 +72,13 @@ describe("runtime-tags/html/content", () => { ); }); + it("should escape { + assert.equal( + helpers._escape_style("foo bar"), + "foo \\3C/style> bar", + ); + }); + it("should allow normally escaped html stuff", () => { assert.equal(helpers._escape_style("foo < bar & baz"), "foo < bar & baz"); }); @@ -84,4 +91,42 @@ describe("runtime-tags/html/content", () => { assert.equal(helpers._escape_style({}), "[object Object]"); }); }); + + describe("escapeScript", () => { + it("should escape { + assert.equal( + helpers._escape_script("foo bar"), + "foo \\x3C/script> bar", + ); + }); + }); + + describe("escapeComment", () => { + it("should return empty string for falseish values", () => { + for (const value of falseishValues) { + assert.equal(helpers._escape_comment(value), ""); + } + }); + + it("should escape > to prevent comment termination", () => { + assert.equal(helpers._escape_comment("-->"), "-->"); + assert.equal(helpers._escape_comment("--!>"), "--!>"); + assert.equal(helpers._escape_comment(">"), ">"); + }); + + it("should allow < and & through unchanged", () => { + assert.equal( + helpers._escape_comment("foo < bar & baz"), + "foo < bar & baz", + ); + }); + + it("should toString anything else", () => { + assert.equal(helpers._escape_comment(0), "0"); + assert.equal(helpers._escape_comment(42), "42"); + assert.equal(helpers._escape_comment(true), "true"); + assert.equal(helpers._escape_comment("foo"), "foo"); + assert.equal(helpers._escape_comment({}), "[object Object]"); + }); + }); }); diff --git a/packages/runtime-tags/src/html.ts b/packages/runtime-tags/src/html.ts index d2a067bcea..2f35ec06ff 100644 --- a/packages/runtime-tags/src/html.ts +++ b/packages/runtime-tags/src/html.ts @@ -25,6 +25,7 @@ export { export { compat } from "./html/compat"; export { _escape, + _escape_comment, _escape_script, _escape_style, _unescaped, diff --git a/packages/runtime-tags/src/html/content.ts b/packages/runtime-tags/src/html/content.ts index 7a3cd607d3..f1cc45b4e0 100644 --- a/packages/runtime-tags/src/html/content.ts +++ b/packages/runtime-tags/src/html/content.ts @@ -10,7 +10,7 @@ export function _escape(val: unknown) { return val ? escapeXMLStr(val + "") : val === 0 ? "0" : ""; } -const unsafeScriptReg = /<\/script/g; +const unsafeScriptReg = /<\/script/gi; const escapeScriptStr = (str: string) => unsafeScriptReg.test(str) ? str.replace(unsafeScriptReg, "\\x3C/script") @@ -19,10 +19,18 @@ export function _escape_script(val: unknown) { return val ? escapeScriptStr(val + "") : val === 0 ? "0" : ""; } -const unsafeStyleReg = /<\/style/g; +const unsafeStyleReg = /<\/style/gi; const escapeStyleStr = (str: string) => unsafeStyleReg.test(str) ? str.replace(unsafeStyleReg, "\\3C/style") : str; export function _escape_style(val: unknown) { return val ? escapeStyleStr(val + "") : val === 0 ? "0" : ""; } + +const unsafeCommentReg = />/g; +const escapeCommentStr = (str: string) => + unsafeCommentReg.test(str) ? str.replace(unsafeCommentReg, ">") : str; + +export function _escape_comment(val: unknown) { + return val ? escapeCommentStr(val + "") : val === 0 ? "0" : ""; +} diff --git a/packages/runtime-tags/src/translator/core/html-comment.ts b/packages/runtime-tags/src/translator/core/html-comment.ts index 0a075dd292..e52652c978 100644 --- a/packages/runtime-tags/src/translator/core/html-comment.ts +++ b/packages/runtime-tags/src/translator/core/html-comment.ts @@ -110,7 +110,7 @@ export default { if (t.isMarkoText(child)) { write`${child.value}`; } else if (t.isMarkoPlaceholder(child)) { - write`${callRuntime("_escape", child.value)}`; + write`${callRuntime(child.escape ? "_escape_comment" : "_unescaped", child.value)}`; } } } else { diff --git a/packages/runtime-tags/src/translator/util/runtime.ts b/packages/runtime-tags/src/translator/util/runtime.ts index e18ed8b9f8..4a6796ab6b 100644 --- a/packages/runtime-tags/src/translator/util/runtime.ts +++ b/packages/runtime-tags/src/translator/util/runtime.ts @@ -7,6 +7,7 @@ import { _attr_class, _attr_style, _escape, + _escape_comment, _escape_script, _escape_style, _unescaped, @@ -71,6 +72,7 @@ export function callRuntime( export function getHTMLRuntime() { return { _escape, + _escape_comment, _unescaped, _attr, _attr_class,