Skip to content

Commit

Permalink
fix: ensure & is escaped for ssr attrs
Browse files Browse the repository at this point in the history
  • Loading branch information
DylanPiercey committed Feb 24, 2025
1 parent a5d8384 commit 6065ff2
Show file tree
Hide file tree
Showing 109 changed files with 252 additions and 257 deletions.
6 changes: 6 additions & 0 deletions .changeset/giant-suits-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"marko": patch
"@marko/runtime-tags": patch
---

Ensure & is escaped for server side attributes to improve consistency with csr.
4 changes: 2 additions & 2 deletions packages/runtime-class/src/core-tags/core/await/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,11 @@ module.exports = function awaitTag(input, out) {
var placeholderIdAttrValue = reorderFunctionId + "ph" + id;

if (placeholderRenderer) {
out.write('<span id="' + placeholderIdAttrValue + '">');
out.write("<span id=" + placeholderIdAttrValue + ">");
placeholderRenderer(out);
out.write("</span>");
} else {
out.write('<noscript id="' + placeholderIdAttrValue + '"></noscript>');
out.write("<noscript id=" + placeholderIdAttrValue + "></noscript>");
}

// If `client-reorder` is enabled then we asynchronously render the await instance to a new
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use strict";

var escapeDoubleQuotes =
require("../../../runtime/html/helpers/escape-quotes").___escapeDoubleQuotes;
var attrAssignment = require("../../../runtime/html/helpers/attr").a;

module.exports = function (input, out) {
// We cannot call beginSync() when using renderSync(). In this case we will
Expand Down Expand Up @@ -94,24 +93,24 @@ module.exports = function (input, out) {

if (global.cspNonce) {
asyncOut.write(
'<style nonce="' +
escapeDoubleQuotes(global.cspNonce) +
'">' +
`#${reorderFunctionId}` +
"<style nonce" +
attrAssignment(global.cspNonce) +
">#" +
reorderFunctionId +
awaitInfo.id +
"{display:none;}" +
"</style>" +
`<div id="${reorderFunctionId}` +
"{display:none}</style><div id=" +
reorderFunctionId +
awaitInfo.id +
'">' +
">" +
result.toString() +
"</div>",
);
} else {
asyncOut.write(
`<div id="${reorderFunctionId}` +
"<div id=" +
reorderFunctionId +
awaitInfo.id +
'" style="display:none">' +
" style=display:none>" +
result.toString() +
"</div>",
);
Expand Down
10 changes: 3 additions & 7 deletions packages/runtime-class/src/runtime/html/StringWriter.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use strict";

var escapeDoubleQuotes =
require("./helpers/escape-quotes").___escapeDoubleQuotes;
var attrAssignment = require("./helpers/attr").a;

function StringWriter() {
this._content = "";
Expand Down Expand Up @@ -59,11 +58,8 @@ StringWriter.prototype = {
this.state.events.emit("___toString", this);
let str = this._content;
if (this._scripts) {
const outGlobal = this.state.root.global;
const cspNonce = outGlobal.cspNonce;
const nonceAttr = cspNonce
? ' nonce="' + escapeDoubleQuotes(cspNonce) + '"'
: "";
const cspNonce = this.state.root.global.cspNonce;
const nonceAttr = cspNonce ? " nonce" + attrAssignment(cspNonce) : "";
str += `<script${nonceAttr}>${this._scripts}</script>`;
}
return str;
Expand Down
91 changes: 53 additions & 38 deletions packages/runtime-class/src/runtime/html/helpers/attr.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
"use strict";

var escapeQuoteHelpers = require("./escape-quotes");
var escapeDoubleQuotes = escapeQuoteHelpers.___escapeDoubleQuotes;
var escapeSingleQuotes = escapeQuoteHelpers.___escapeSingleQuotes;
// eslint-disable-next-line no-constant-binary-expression
var complain = "MARKO_DEBUG" && require("complain");

module.exports = maybeEmptyAttr;
module.exports = attr;

maybeEmptyAttr.___notEmptyAttr = notEmptyAttr;
maybeEmptyAttr.___isEmptyAttrValue = isEmpty;
attr.___notEmptyAttr = nonVoidAttr;
attr.___isEmptyAttrValue = isVoid;
attr.a = attrAssignment;
attr.d = escapeDoubleQuotedAttrValue;
attr.s = escapeSingleQuotedAttrValue;

function maybeEmptyAttr(name, value) {
if (isEmpty(value)) {
return "";
}

return notEmptyAttr(name, value);
function attr(name, value) {
return isVoid(value) ? "" : nonVoidAttr(name, value);
}

function notEmptyAttr(name, value) {
function nonVoidAttr(name, value) {
switch (typeof value) {
case "string":
return " " + name + guessQuotes(value);
return " " + name + attrAssignment(value);
case "boolean":
return " " + name;
case "number":
Expand All @@ -39,42 +35,61 @@ function notEmptyAttr(name, value) {
);
}

return " " + name + singleQuote(JSON.stringify(value), 2);
return (
" " +
name +
"='" +
escapeSingleQuotedAttrValue(JSON.stringify(value)) +
"'"
);
case RegExp.prototype.toString:
return " " + name + guessQuotes(value.source);
return " " + name + attrAssignment(value.source);
}
}

return " " + name + guessQuotes(value + "");
return " " + name + attrAssignment(value + "");
}

function isEmpty(value) {
function isVoid(value) {
return value == null || value === false;
}

function doubleQuote(value, startPos) {
return '="' + escapeDoubleQuotes(value, startPos) + '"';
var singleQuoteAttrReplacements = /'|&(?=#?\w+;)/g;
var doubleQuoteAttrReplacements = /"|&(?=#?\w+;)/g;
var needsQuotedAttr = /["'>\s]|&#?\w+;|\/$/g;
function attrAssignment(value) {
return value
? needsQuotedAttr.test(value)
? value[needsQuotedAttr.lastIndex - 1] ===
((needsQuotedAttr.lastIndex = 0), '"')
? "='" + escapeSingleQuotedAttrValue(value) + "'"
: '="' + escapeDoubleQuotedAttrValue(value) + '"'
: "=" + value
: "";
}

function singleQuote(value, startPos) {
return "='" + escapeSingleQuotes(value, startPos) + "'";
function escapeSingleQuotedAttrValue(value) {
return singleQuoteAttrReplacements.test(value)
? value.replace(
singleQuoteAttrReplacements,
replaceUnsafeSingleQuoteAttrChar,
)
: value;
}

function guessQuotes(value) {
for (var i = 0, len = value.length; i < len; i++) {
switch (value[i]) {
case '"':
return singleQuote(value, i + 1);
case "'":
case ">":
case " ":
case "\t":
case "\n":
case "\r":
case "\f":
return doubleQuote(value, i + 1);
}
}
function replaceUnsafeSingleQuoteAttrChar(match) {
return match === "'" ? "&#39;" : "&amp;";
}

function escapeDoubleQuotedAttrValue(value) {
return doubleQuoteAttrReplacements.test(value)
? value.replace(
doubleQuoteAttrReplacements,
replaceUnsafeDoubleQuoteAttrChar,
)
: value;
}

return value && "=" + (value[len - 1] === "/" ? value + " " : value);
function replaceUnsafeDoubleQuoteAttrChar(match) {
return match === '"' ? "&#34;" : "&amp;";
}
6 changes: 3 additions & 3 deletions packages/runtime-class/src/runtime/html/helpers/data-marko.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use strict";

var escapeQuoteHelpers = require("./escape-quotes");
var escapeSingleQuotes = escapeQuoteHelpers.___escapeSingleQuotes;
var escapeDoubleQuotes = escapeQuoteHelpers.___escapeDoubleQuotes;
var attr = require("./attr");
var escapeSingleQuotes = attr.s;
var escapeDoubleQuotes = attr.d;
var FLAG_WILL_RERENDER_IN_BROWSER = 1;
// var FLAG_HAS_RENDER_BODY = 2;

Expand Down
35 changes: 0 additions & 35 deletions packages/runtime-class/src/runtime/html/helpers/escape-quotes.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use strict";

var escapeDoubleQuotes = require("./escape-quotes").___escapeDoubleQuotes;
var attrAssignment = require("./attr").a;
var escapeScript = require("./escape-script-placeholder");
var assignPropsFunction = `
function ap_(p) {
Expand All @@ -17,9 +17,7 @@ var assignPropsFunction = `

module.exports = function propsForPreviousNode(props, out) {
var cspNonce = out.global.cspNonce;
var nonceAttr = cspNonce
? ' nonce="' + escapeDoubleQuotes(cspNonce) + '"'
: "";
var nonceAttr = cspNonce ? " nonce" + attrAssignment(cspNonce) : "";

out.w("<script" + nonceAttr + ">");

Expand Down
1 change: 0 additions & 1 deletion packages/runtime-class/src/translator/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,6 @@ export function getRuntimeEntryFiles(output, optimize) {
`${base}runtime/html/helpers/attrs.js`,
`${base}runtime/html/helpers/class-attr.js`,
`${base}runtime/html/helpers/data-marko.js`,
`${base}runtime/html/helpers/escape-quotes.js`,
`${base}runtime/html/helpers/escape-script-placeholder.js`,
`${base}runtime/html/helpers/escape-style-placeholder.js`,
`${base}runtime/html/helpers/escape-xml.js`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
normalizeTemplateString,
} from "@marko/compiler/babel-utils";
import attrHelper from "marko/src/runtime/html/helpers/attr";
import { d as escapeDoubleQuotes } from "marko/src/runtime/html/helpers/escape-quotes";

import { evaluateAttr } from "../util";

Expand Down Expand Up @@ -105,26 +104,26 @@ export default function (path, attrs) {
for (let i = 0; i < value.expressions.length; i++) {
const quasi = value.quasis[i];
const expression = value.expressions[i];
curString += escapeDoubleQuotes(quasi.value.cooked);
curString += attrHelper.d(quasi.value.cooked);
quasis.push(curString);
curString = "";
expressions.push(
t.callExpression(
importNamed(
file,
"marko/src/runtime/html/helpers/escape-quotes.js",
"d",
"marko_escape_double_quotes",
t.memberExpression(
importDefault(
file,
"marko/src/runtime/html/helpers/attr.js",
"marko_attr",
),
t.identifier("d"),
),
[expression],
),
);
}

curString +=
escapeDoubleQuotes(
value.quasis[value.expressions.length].value.cooked,
) + '"';
attrHelper.d(value.quasis[value.expressions.length].value.cooked) + '"';
} else {
quasis.push(curString);
curString = "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module.exports = function (helpers, done) {
var template = require("./template.marko").default;

template.render({ $global: { cspNonce: "abc123" } }, function (err, html) {
if (!/<script.*nonce="abc123".*>/.test(html)) {
if (!/<script.*nonce="?abc123"?.*>/.test(html)) {
throw new Error("script tag does not contain a nonce");
}
done();
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<noscript id="afph0"></noscript><!--FLUSH--><style nonce="xyz">#af0{display:none;}</style><div id="af0"><div class=foo><h1>Foo</h1> Hello World</div></div><script nonce="xyz">function $af(d,a,e,l,g,h,k,b,f,c){c=$af;if(a&&!c[a])(c[a+="$"]||(c[a]=[])).push(d);else{e=document;l=e.getElementById("af"+d);g=e.getElementById("afph"+d);h=e.createDocumentFragment();k=l.childNodes;b=0;for(f=k.length;b<f;b++)h.appendChild(k.item(0));g&&g.parentNode.replaceChild(h,g);c[d]=1;if(a=c[d+"$"])for(b=0,f=a.length;b<f;b++)c(a[b])}};$af(0)</script>
<noscript id=afph0></noscript><!--FLUSH--><style nonce=xyz>#af0{display:none}</style><div id=af0><div class=foo><h1>Foo</h1> Hello World</div></div><script nonce=xyz>function $af(d,a,e,l,g,h,k,b,f,c){c=$af;if(a&&!c[a])(c[a+="$"]||(c[a]=[])).push(d);else{e=document;l=e.getElementById("af"+d);g=e.getElementById("afph"+d);h=e.createDocumentFragment();k=l.childNodes;b=0;for(f=k.length;b<f;b++)h.appendChild(k.item(0));g&&g.parentNode.replaceChild(h,g);c[d]=1;if(a=c[d+"$"])for(b=0,f=a.length;b<f;b++)c(a[b])}};$af(0)</script>
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<body><noscript id="afph0"></noscript><!--FLUSH--><div id="af0" style="display:none"><div class=foo><h1>Foo</h1> Hello World</div></div><script>function $af(d,a,e,l,g,h,k,b,f,c){c=$af;if(a&&!c[a])(c[a+="$"]||(c[a]=[])).push(d);else{e=document;l=e.getElementById("af"+d);g=e.getElementById("afph"+d);h=e.createDocumentFragment();k=l.childNodes;b=0;for(f=k.length;b<f;b++)h.appendChild(k.item(0));g&&g.parentNode.replaceChild(h,g);c[d]=1;if(a=c[d+"$"])for(b=0,f=a.length;b<f;b++)c(a[b])}};$af(0)</script></body>
<body><noscript id=afph0></noscript><!--FLUSH--><div id=af0 style=display:none><div class=foo><h1>Foo</h1> Hello World</div></div><script>function $af(d,a,e,l,g,h,k,b,f,c){c=$af;if(a&&!c[a])(c[a+="$"]||(c[a]=[])).push(d);else{e=document;l=e.getElementById("af"+d);g=e.getElementById("afph"+d);h=e.createDocumentFragment();k=l.childNodes;b=0;for(f=k.length;b<f;b++)h.appendChild(k.item(0));g&&g.parentNode.replaceChild(h,g);c[d]=1;if(a=c[d+"$"])for(b=0,f=a.length;b<f;b++)c(a[b])}};$af(0)</script></body>
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<body><noscript id="afph0"></noscript><!--FLUSH--><div id="af0" style="display:none"><div class=outer><h1>Outer</h1><div class=inner><h2>Inner 1</h2></div></div></div><script>function $af(d,a,e,l,g,h,k,b,f,c){c=$af;if(a&&!c[a])(c[a+="$"]||(c[a]=[])).push(d);else{e=document;l=e.getElementById("af"+d);g=e.getElementById("afph"+d);h=e.createDocumentFragment();k=l.childNodes;b=0;for(f=k.length;b<f;b++)h.appendChild(k.item(0));g&&g.parentNode.replaceChild(h,g);c[d]=1;if(a=c[d+"$"])for(b=0,f=a.length;b<f;b++)c(a[b])}};$af(0)</script></body>
<body><noscript id=afph0></noscript><!--FLUSH--><div id=af0 style=display:none><div class=outer><h1>Outer</h1><div class=inner><h2>Inner 1</h2></div></div></div><script>function $af(d,a,e,l,g,h,k,b,f,c){c=$af;if(a&&!c[a])(c[a+="$"]||(c[a]=[])).push(d);else{e=document;l=e.getElementById("af"+d);g=e.getElementById("afph"+d);h=e.createDocumentFragment();k=l.childNodes;b=0;for(f=k.length;b<f;b++)h.appendChild(k.item(0));g&&g.parentNode.replaceChild(h,g);c[d]=1;if(a=c[d+"$"])for(b=0,f=a.length;b<f;b++)c(a[b])}};$af(0)</script></body>
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<noscript id="afph0"></noscript><script type="text/javascript">function $af(d,a,e,l,g,h,k,b,f,c){c=$af;if(a&&!c[a])(c[a+="$"]||(c[a]=[])).push(d);else{e=document;l=e.getElementById("af"+d);g=e.getElementById("afph"+d);h=e.createDocumentFragment();k=l.childNodes;b=0;for(f=k.length;b<f;b++)h.appendChild(k.item(0));g.parentNode.replaceChild(h,g);c[d]=1;if(a=c[d+"$"])for(b=0,f=a.length;b<f;b++)c(a[b])}};</script><div id="af0" style="display:none"><div class="outer"><h1>Outer</h1><noscript id="afph1"></noscript><noscript id="afph2"></noscript></div></div><script type="text/javascript">$af(0)</script><div id="af1" style="display:none"><div class="inner1"><h2>Inner 1</h2></div></div><script type="text/javascript">$af(1)</script><div id="af2" style="display:none"><div class="inner2"><h2>Inner 2</h2></div></div><script type="text/javascript">$af(2)</script>
<noscript id=afph0></noscript><!--FLUSH--><div id=af0 style=display:none><div class=outer><h1>Outer</h1><noscript id=afph1></noscript><noscript id=afph2></noscript></div></div><script>function $af(d,a,e,l,g,h,k,b,f,c){c=$af;if(a&&!c[a])(c[a+="$"]||(c[a]=[])).push(d);else{e=document;l=e.getElementById("af"+d);g=e.getElementById("afph"+d);h=e.createDocumentFragment();k=l.childNodes;b=0;for(f=k.length;b<f;b++)h.appendChild(k.item(0));g&&g.parentNode.replaceChild(h,g);c[d]=1;if(a=c[d+"$"])for(b=0,f=a.length;b<f;b++)c(a[b])}};$af(0)</script><!--FLUSH--><div id=af1 style=display:none><div class=inner1><h2>Inner 1</h2></div></div><script>$af(1)</script><!--FLUSH--><div id=af2 style=display:none><div class=inner2><h2>Inner 2</h2></div></div><script>$af(2)</script>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ exports.templateData = {
inner2: callbackProvider(3, {}),
};

exports.checkHtml = function () {};
exports.checkEvents = function (events, snapshot, out) {
events = events.map(function (eventInfo) {
var arg = extend({}, eventInfo.arg);
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<noscript id="afph0"></noscript><!--FLUSH--><div id="af0" style="display:none"><div class=foo><h1>Foo</h1> Hello World</div></div><script>function $af(d,a,e,l,g,h,k,b,f,c){c=$af;if(a&&!c[a])(c[a+="$"]||(c[a]=[])).push(d);else{e=document;l=e.getElementById("af"+d);g=e.getElementById("afph"+d);h=e.createDocumentFragment();k=l.childNodes;b=0;for(f=k.length;b<f;b++)h.appendChild(k.item(0));g&&g.parentNode.replaceChild(h,g);c[d]=1;if(a=c[d+"$"])for(b=0,f=a.length;b<f;b++)c(a[b])}};$af(0)</script>
<noscript id=afph0></noscript><!--FLUSH--><div id=af0 style=display:none><div class=foo><h1>Foo</h1> Hello World</div></div><script>function $af(d,a,e,l,g,h,k,b,f,c){c=$af;if(a&&!c[a])(c[a+="$"]||(c[a]=[])).push(d);else{e=document;l=e.getElementById("af"+d);g=e.getElementById("afph"+d);h=e.createDocumentFragment();k=l.childNodes;b=0;for(f=k.length;b<f;b++)h.appendChild(k.item(0));g&&g.parentNode.replaceChild(h,g);c[d]=1;if(a=c[d+"$"])for(b=0,f=a.length;b<f;b++)c(a[b])}};$af(0)</script>
Original file line number Diff line number Diff line change
@@ -1 +1 @@
BEFORE-OUT-OF-ORDER <noscript id="afpha"></noscript><!--FLUSH-->BEFORE-AFTER-OUT-OF-ORDER AFTER-OUT-OF-ORDER <div id="afa" style="display:none">BEFORE-IN-ORDER INSIDE-IN-ORDER<noscript id="afphc"></noscript> AFTER-IN-ORDER</div><div id="afc" style="display:none">NESTED-OUT-OF-ORDER</div><script>function $af(d,a,e,l,g,h,k,b,f,c){c=$af;if(a&&!c[a])(c[a+="$"]||(c[a]=[])).push(d);else{e=document;l=e.getElementById("af"+d);g=e.getElementById("afph"+d);h=e.createDocumentFragment();k=l.childNodes;b=0;for(f=k.length;b<f;b++)h.appendChild(k.item(0));g&&g.parentNode.replaceChild(h,g);c[d]=1;if(a=c[d+"$"])for(b=0,f=a.length;b<f;b++)c(a[b])}};$af("a");$af("c")</script>
BEFORE-OUT-OF-ORDER <noscript id=afpha></noscript><!--FLUSH-->BEFORE-AFTER-OUT-OF-ORDER AFTER-OUT-OF-ORDER <div id=afa style=display:none>BEFORE-IN-ORDER INSIDE-IN-ORDER<noscript id=afphc></noscript> AFTER-IN-ORDER</div><div id=afc style=display:none>NESTED-OUT-OF-ORDER</div><script>function $af(d,a,e,l,g,h,k,b,f,c){c=$af;if(a&&!c[a])(c[a+="$"]||(c[a]=[])).push(d);else{e=document;l=e.getElementById("af"+d);g=e.getElementById("afph"+d);h=e.createDocumentFragment();k=l.childNodes;b=0;for(f=k.length;b<f;b++)h.appendChild(k.item(0));g&&g.parentNode.replaceChild(h,g);c[d]=1;if(a=c[d+"$"])for(b=0,f=a.length;b<f;b++)c(a[b])}};$af("a");$af("c")</script>
Original file line number Diff line number Diff line change
@@ -1 +1 @@
BEFORE-OUT-OF-ORDER <noscript id="afpha"></noscript> AFTER-OUT-OF-ORDER <!--FLUSH--><div id="afc" style="display:none">NESTED-OUT-OF-ORDER</div><script>function $af(d,a,e,l,g,h,k,b,f,c){c=$af;if(a&&!c[a])(c[a+="$"]||(c[a]=[])).push(d);else{e=document;l=e.getElementById("af"+d);g=e.getElementById("afph"+d);h=e.createDocumentFragment();k=l.childNodes;b=0;for(f=k.length;b<f;b++)h.appendChild(k.item(0));g&&g.parentNode.replaceChild(h,g);c[d]=1;if(a=c[d+"$"])for(b=0,f=a.length;b<f;b++)c(a[b])}}</script><!--FLUSH--><div id="afa" style="display:none">BEFORE-IN-ORDER INSIDE-IN-ORDER<noscript id="afphc"></noscript> AFTER-IN-ORDER</div><script>$af("a");$af("c")</script>
BEFORE-OUT-OF-ORDER <noscript id=afpha></noscript> AFTER-OUT-OF-ORDER <!--FLUSH--><div id=afc style=display:none>NESTED-OUT-OF-ORDER</div><script>function $af(d,a,e,l,g,h,k,b,f,c){c=$af;if(a&&!c[a])(c[a+="$"]||(c[a]=[])).push(d);else{e=document;l=e.getElementById("af"+d);g=e.getElementById("afph"+d);h=e.createDocumentFragment();k=l.childNodes;b=0;for(f=k.length;b<f;b++)h.appendChild(k.item(0));g&&g.parentNode.replaceChild(h,g);c[d]=1;if(a=c[d+"$"])for(b=0,f=a.length;b<f;b++)c(a[b])}}</script><!--FLUSH--><div id=afa style=display:none>BEFORE-IN-ORDER INSIDE-IN-ORDER<noscript id=afphc></noscript> AFTER-IN-ORDER</div><script>$af("a");$af("c")</script>
Loading

0 comments on commit 6065ff2

Please sign in to comment.