Fix Go template script escaping

Fixes #6695
This commit is contained in:
Bjørn Erik Pedersen
2020-05-23 15:32:27 +02:00
parent c34bf48560
commit 6c3c6686f5
27 changed files with 1090 additions and 292 deletions

View File

@@ -21,7 +21,7 @@ func TestTypedContent(t *testing.T) {
htmltemplate.HTML(`Hello, <b>World</b> &amp;tc!`),
htmltemplate.HTMLAttr(` dir="ltr"`),
htmltemplate.JS(`c && alert("Hello, World!");`),
htmltemplate.JSStr(`Hello, World & O'Reilly\x21`),
htmltemplate.JSStr(`Hello, World & O'Reilly\u0021`),
htmltemplate.URL(`greeting=H%69,&addressee=(World)`),
htmltemplate.Srcset(`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`),
htmltemplate.URL(`,foo/,`),
@@ -73,7 +73,7 @@ func TestTypedContent(t *testing.T) {
`Hello, <b>World</b> &amp;tc!`,
` dir=&#34;ltr&#34;`,
`c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
`Hello, World &amp; O&#39;Reilly\x21`,
`Hello, World &amp; O&#39;Reilly\u0021`,
`greeting=H%69,&amp;addressee=(World)`,
`greeting=H%69,&amp;addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
`,foo/,`,
@@ -103,7 +103,7 @@ func TestTypedContent(t *testing.T) {
`Hello,&#32;World&#32;&amp;tc!`,
`&#32;dir&#61;&#34;ltr&#34;`,
`c&#32;&amp;&amp;&#32;alert(&#34;Hello,&#32;World!&#34;);`,
`Hello,&#32;World&#32;&amp;&#32;O&#39;Reilly\x21`,
`Hello,&#32;World&#32;&amp;&#32;O&#39;Reilly\u0021`,
`greeting&#61;H%69,&amp;addressee&#61;(World)`,
`greeting&#61;H%69,&amp;addressee&#61;(World)&#32;2x,&#32;https://golang.org/favicon.ico&#32;500.5w`,
`,foo/,`,
@@ -118,7 +118,7 @@ func TestTypedContent(t *testing.T) {
`Hello, World &amp;tc!`,
` dir=&#34;ltr&#34;`,
`c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
`Hello, World &amp; O&#39;Reilly\x21`,
`Hello, World &amp; O&#39;Reilly\u0021`,
`greeting=H%69,&amp;addressee=(World)`,
`greeting=H%69,&amp;addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
`,foo/,`,
@@ -133,7 +133,7 @@ func TestTypedContent(t *testing.T) {
`Hello, &lt;b&gt;World&lt;/b&gt; &amp;tc!`,
` dir=&#34;ltr&#34;`,
`c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
`Hello, World &amp; O&#39;Reilly\x21`,
`Hello, World &amp; O&#39;Reilly\u0021`,
`greeting=H%69,&amp;addressee=(World)`,
`greeting=H%69,&amp;addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
`,foo/,`,
@@ -149,7 +149,7 @@ func TestTypedContent(t *testing.T) {
// Not escaped.
`c && alert("Hello, World!");`,
// Escape sequence not over-escaped.
`"Hello, World & O'Reilly\x21"`,
`"Hello, World & O'Reilly\u0021"`,
`"greeting=H%69,\u0026addressee=(World)"`,
`"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`,
`",foo/,"`,
@@ -165,7 +165,7 @@ func TestTypedContent(t *testing.T) {
// Not JS escaped but HTML escaped.
`c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
// Escape sequence not over-escaped.
`&#34;Hello, World &amp; O&#39;Reilly\x21&#34;`,
`&#34;Hello, World &amp; O&#39;Reilly\u0021&#34;`,
`&#34;greeting=H%69,\u0026addressee=(World)&#34;`,
`&#34;greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w&#34;`,
`&#34;,foo/,&#34;`,
@@ -174,30 +174,30 @@ func TestTypedContent(t *testing.T) {
{
`<script>alert("{{.}}")</script>`,
[]string{
`\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`,
`a[href =~ \x22\/\/example.com\x22]#foo`,
`Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`,
` dir=\x22ltr\x22`,
`c \x26\x26 alert(\x22Hello, World!\x22);`,
`\u003cb\u003e \u0022foo%\u0022 O\u0027Reilly \u0026bar;`,
`a[href =~ \u0022\/\/example.com\u0022]#foo`,
`Hello, \u003cb\u003eWorld\u003c\/b\u003e \u0026amp;tc!`,
` dir=\u0022ltr\u0022`,
`c \u0026\u0026 alert(\u0022Hello, World!\u0022);`,
// Escape sequence not over-escaped.
`Hello, World \x26 O\x27Reilly\x21`,
`greeting=H%69,\x26addressee=(World)`,
`greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
`Hello, World \u0026 O\u0027Reilly\u0021`,
`greeting=H%69,\u0026addressee=(World)`,
`greeting=H%69,\u0026addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
`,foo\/,`,
},
},
{
`<script type="text/javascript">alert("{{.}}")</script>`,
[]string{
`\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`,
`a[href =~ \x22\/\/example.com\x22]#foo`,
`Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`,
` dir=\x22ltr\x22`,
`c \x26\x26 alert(\x22Hello, World!\x22);`,
`\u003cb\u003e \u0022foo%\u0022 O\u0027Reilly \u0026bar;`,
`a[href =~ \u0022\/\/example.com\u0022]#foo`,
`Hello, \u003cb\u003eWorld\u003c\/b\u003e \u0026amp;tc!`,
` dir=\u0022ltr\u0022`,
`c \u0026\u0026 alert(\u0022Hello, World!\u0022);`,
// Escape sequence not over-escaped.
`Hello, World \x26 O\x27Reilly\x21`,
`greeting=H%69,\x26addressee=(World)`,
`greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
`Hello, World \u0026 O\u0027Reilly\u0021`,
`greeting=H%69,\u0026addressee=(World)`,
`greeting=H%69,\u0026addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
`,foo\/,`,
},
},
@@ -211,7 +211,7 @@ func TestTypedContent(t *testing.T) {
// Not escaped.
`c && alert("Hello, World!");`,
// Escape sequence not over-escaped.
`"Hello, World & O'Reilly\x21"`,
`"Hello, World & O'Reilly\u0021"`,
`"greeting=H%69,\u0026addressee=(World)"`,
`"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`,
`",foo/,"`,
@@ -227,7 +227,7 @@ func TestTypedContent(t *testing.T) {
`Hello, <b>World</b> &amp;tc!`,
` dir=&#34;ltr&#34;`,
`c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
`Hello, World &amp; O&#39;Reilly\x21`,
`Hello, World &amp; O&#39;Reilly\u0021`,
`greeting=H%69,&amp;addressee=(World)`,
`greeting=H%69,&amp;addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
`,foo/,`,
@@ -236,15 +236,15 @@ func TestTypedContent(t *testing.T) {
{
`<button onclick='alert("{{.}}")'>`,
[]string{
`\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`,
`a[href =~ \x22\/\/example.com\x22]#foo`,
`Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`,
` dir=\x22ltr\x22`,
`c \x26\x26 alert(\x22Hello, World!\x22);`,
`\u003cb\u003e \u0022foo%\u0022 O\u0027Reilly \u0026bar;`,
`a[href =~ \u0022\/\/example.com\u0022]#foo`,
`Hello, \u003cb\u003eWorld\u003c\/b\u003e \u0026amp;tc!`,
` dir=\u0022ltr\u0022`,
`c \u0026\u0026 alert(\u0022Hello, World!\u0022);`,
// Escape sequence not over-escaped.
`Hello, World \x26 O\x27Reilly\x21`,
`greeting=H%69,\x26addressee=(World)`,
`greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
`Hello, World \u0026 O\u0027Reilly\u0021`,
`greeting=H%69,\u0026addressee=(World)`,
`greeting=H%69,\u0026addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
`,foo\/,`,
},
},
@@ -256,7 +256,7 @@ func TestTypedContent(t *testing.T) {
`Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21`,
`%20dir%3d%22ltr%22`,
`c%20%26%26%20alert%28%22Hello%2c%20World%21%22%29%3b`,
`Hello%2c%20World%20%26%20O%27Reilly%5cx21`,
`Hello%2c%20World%20%26%20O%27Reilly%5cu0021`,
// Quotes and parens are escaped but %69 is not over-escaped. HTML escaping is done.
`greeting=H%69,&amp;addressee=%28World%29`,
`greeting%3dH%2569%2c%26addressee%3d%28World%29%202x%2c%20https%3a%2f%2fgolang.org%2ffavicon.ico%20500.5w`,
@@ -271,7 +271,7 @@ func TestTypedContent(t *testing.T) {
`Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21`,
`%20dir%3d%22ltr%22`,
`c%20%26%26%20alert%28%22Hello%2c%20World%21%22%29%3b`,
`Hello%2c%20World%20%26%20O%27Reilly%5cx21`,
`Hello%2c%20World%20%26%20O%27Reilly%5cu0021`,
// Quotes and parens are escaped but %69 is not over-escaped. HTML escaping is not done.
`greeting=H%69,&addressee=%28World%29`,
`greeting%3dH%2569%2c%26addressee%3d%28World%29%202x%2c%20https%3a%2f%2fgolang.org%2ffavicon.ico%20500.5w`,

View File

@@ -73,6 +73,51 @@ functions.
For these internal escaping functions, if an action pipeline evaluates to
a nil interface value, it is treated as though it were an empty string.
Namespaced and data- attributes
Attributes with a namespace are treated as if they had no namespace.
Given the excerpt
<a my:href="{{.}}"></a>
At parse time the attribute will be treated as if it were just "href".
So at parse time the template becomes:
<a my:href="{{. | urlescaper | attrescaper}}"></a>
Similarly to attributes with namespaces, attributes with a "data-" prefix are
treated as if they had no "data-" prefix. So given
<a data-href="{{.}}"></a>
At parse time this becomes
<a data-href="{{. | urlescaper | attrescaper}}"></a>
If an attribute has both a namespace and a "data-" prefix, only the namespace
will be removed when determining the context. For example
<a my:data-href="{{.}}"></a>
This is handled as if "my:data-href" was just "data-href" and not "href" as
it would be if the "data-" prefix were to be ignored too. Thus at parse
time this becomes just
<a my:data-href="{{. | attrescaper}}"></a>
As a special case, attributes with the namespace "xmlns" are always treated
as containing URLs. Given the excerpts
<a xmlns:title="{{.}}"></a>
<a xmlns:href="{{.}}"></a>
<a xmlns:onclick="{{.}}"></a>
At parse time they become:
<a xmlns:title="{{. | urlescaper | attrescaper}}"></a>
<a xmlns:href="{{. | urlescaper | attrescaper}}"></a>
<a xmlns:onclick="{{. | urlescaper | attrescaper}}"></a>
Errors
See the documentation of ErrorCode for details.

View File

@@ -242,7 +242,7 @@ func TestEscape(t *testing.T) {
{
"jsStr",
"<button onclick='alert(&quot;{{.H}}&quot;)'>",
`<button onclick='alert(&quot;\x3cHello\x3e&quot;)'>`,
`<button onclick='alert(&quot;\u003cHello\u003e&quot;)'>`,
},
{
"badMarshaler",
@@ -263,7 +263,7 @@ func TestEscape(t *testing.T) {
{
"jsRe",
`<button onclick='alert(/{{"foo+bar"}}/.test(""))'>`,
`<button onclick='alert(/foo\x2bbar/.test(""))'>`,
`<button onclick='alert(/foo\u002bbar/.test(""))'>`,
},
{
"jsReBlank",
@@ -829,7 +829,7 @@ func TestEscapeSet(t *testing.T) {
"main": `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`,
"helper": `{{11}} of {{"<100>"}}`,
},
`<button onclick="title='11 of \x3c100\x3e'; ...">11 of &lt;100&gt;</button>`,
`<button onclick="title='11 of \u003c100\u003e'; ...">11 of &lt;100&gt;</button>`,
},
// A non-recursive template that ends in a different context.
// helper starts in jsCtxRegexp and ends in jsCtxDivOp.

View File

@@ -119,9 +119,9 @@ func Example_escape() {
// &#34;Fran &amp; Freddie&#39;s Diner&#34; &lt;tasty@example.com&gt;
// &#34;Fran &amp; Freddie&#39;s Diner&#34; &lt;tasty@example.com&gt;
// &#34;Fran &amp; Freddie&#39;s Diner&#34;32&lt;tasty@example.com&gt;
// \"Fran & Freddie\'s Diner\" \x3Ctasty@example.com\x3E
// \"Fran & Freddie\'s Diner\" \x3Ctasty@example.com\x3E
// \"Fran & Freddie\'s Diner\"32\x3Ctasty@example.com\x3E
// \"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
// \"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
// \"Fran \u0026 Freddie\'s Diner\"32\u003Ctasty@example.com\u003E
// %22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E
}

View File

@@ -164,7 +164,6 @@ func jsValEscaper(args ...interface{}) string {
}
// TODO: detect cycles before calling Marshal which loops infinitely on
// cyclic data. This may be an unacceptable DoS risk.
b, err := json.Marshal(a)
if err != nil {
// Put a space before comment so that if it is flush against
@@ -179,8 +178,8 @@ func jsValEscaper(args ...interface{}) string {
// TODO: maybe post-process output to prevent it from containing
// "<!--", "-->", "<![CDATA[", "]]>", or "</script"
// in case custom marshalers produce output containing those.
// TODO: Maybe abbreviate \u00ab to \xab to produce more compact output.
// Note: Do not use \x escaping to save bytes because it is not JSON compatible and this escaper
// supports ld+json content-type.
if len(b) == 0 {
// In, `x=y/{{.}}*z` a json.Marshaler that produces "" should
// not cause the output `x=y/*z`.
@@ -261,6 +260,8 @@ func replace(s string, replacementTable []string) string {
r, w = utf8.DecodeRuneInString(s[i:])
var repl string
switch {
case int(r) < len(lowUnicodeReplacementTable):
repl = lowUnicodeReplacementTable[r]
case int(r) < len(replacementTable) && replacementTable[r] != "":
repl = replacementTable[r]
case r == '\u2028':
@@ -284,67 +285,80 @@ func replace(s string, replacementTable []string) string {
return b.String()
}
var jsStrReplacementTable = []string{
0: `\0`,
var lowUnicodeReplacementTable = []string{
0: `\u0000`, 1: `\u0001`, 2: `\u0002`, 3: `\u0003`, 4: `\u0004`, 5: `\u0005`, 6: `\u0006`,
'\a': `\u0007`,
'\b': `\u0008`,
'\t': `\t`,
'\n': `\n`,
'\v': `\x0b`, // "\v" == "v" on IE 6.
'\v': `\u000b`, // "\v" == "v" on IE 6.
'\f': `\f`,
'\r': `\r`,
0xe: `\u000e`, 0xf: `\u000f`, 0x10: `\u0010`, 0x11: `\u0011`, 0x12: `\u0012`, 0x13: `\u0013`,
0x14: `\u0014`, 0x15: `\u0015`, 0x16: `\u0016`, 0x17: `\u0017`, 0x18: `\u0018`, 0x19: `\u0019`,
0x1a: `\u001a`, 0x1b: `\u001b`, 0x1c: `\u001c`, 0x1d: `\u001d`, 0x1e: `\u001e`, 0x1f: `\u001f`,
}
var jsStrReplacementTable = []string{
0: `\u0000`,
'\t': `\t`,
'\n': `\n`,
'\v': `\u000b`, // "\v" == "v" on IE 6.
'\f': `\f`,
'\r': `\r`,
// Encode HTML specials as hex so the output can be embedded
// in HTML attributes without further encoding.
'"': `\x22`,
'&': `\x26`,
'\'': `\x27`,
'+': `\x2b`,
'"': `\u0022`,
'&': `\u0026`,
'\'': `\u0027`,
'+': `\u002b`,
'/': `\/`,
'<': `\x3c`,
'>': `\x3e`,
'<': `\u003c`,
'>': `\u003e`,
'\\': `\\`,
}
// jsStrNormReplacementTable is like jsStrReplacementTable but does not
// overencode existing escapes since this table has no entry for `\`.
var jsStrNormReplacementTable = []string{
0: `\0`,
0: `\u0000`,
'\t': `\t`,
'\n': `\n`,
'\v': `\x0b`, // "\v" == "v" on IE 6.
'\v': `\u000b`, // "\v" == "v" on IE 6.
'\f': `\f`,
'\r': `\r`,
// Encode HTML specials as hex so the output can be embedded
// in HTML attributes without further encoding.
'"': `\x22`,
'&': `\x26`,
'\'': `\x27`,
'+': `\x2b`,
'"': `\u0022`,
'&': `\u0026`,
'\'': `\u0027`,
'+': `\u002b`,
'/': `\/`,
'<': `\x3c`,
'>': `\x3e`,
'<': `\u003c`,
'>': `\u003e`,
}
var jsRegexpReplacementTable = []string{
0: `\0`,
0: `\u0000`,
'\t': `\t`,
'\n': `\n`,
'\v': `\x0b`, // "\v" == "v" on IE 6.
'\v': `\u000b`, // "\v" == "v" on IE 6.
'\f': `\f`,
'\r': `\r`,
// Encode HTML specials as hex so the output can be embedded
// in HTML attributes without further encoding.
'"': `\x22`,
'"': `\u0022`,
'$': `\$`,
'&': `\x26`,
'\'': `\x27`,
'&': `\u0026`,
'\'': `\u0027`,
'(': `\(`,
')': `\)`,
'*': `\*`,
'+': `\x2b`,
'+': `\u002b`,
'-': `\-`,
'.': `\.`,
'/': `\/`,
'<': `\x3c`,
'>': `\x3e`,
'<': `\u003c`,
'>': `\u003e`,
'?': `\?`,
'[': `\[`,
'\\': `\\`,
@@ -384,11 +398,11 @@ func isJSType(mimeType string) bool {
// https://tools.ietf.org/html/rfc7231#section-3.1.1
// https://tools.ietf.org/html/rfc4329#section-3
// https://www.ietf.org/rfc/rfc4627.txt
mimeType = strings.ToLower(mimeType)
// discard parameters
if i := strings.Index(mimeType, ";"); i >= 0 {
mimeType = mimeType[:i]
}
mimeType = strings.ToLower(mimeType)
mimeType = strings.TrimSpace(mimeType)
switch mimeType {
case

View File

@@ -139,7 +139,7 @@ func TestJSValEscaper(t *testing.T) {
{"foo", `"foo"`},
// Newlines.
{"\r\n\u2028\u2029", `"\r\n\u2028\u2029"`},
// "\v" == "v" on IE 6 so use "\x0b" instead.
// "\v" == "v" on IE 6 so use "\u000b" instead.
{"\t\x0b", `"\t\u000b"`},
{struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`},
{[]interface{}{}, "[]"},
@@ -175,7 +175,7 @@ func TestJSStrEscaper(t *testing.T) {
}{
{"", ``},
{"foo", `foo`},
{"\u0000", `\0`},
{"\u0000", `\u0000`},
{"\t", `\t`},
{"\n", `\n`},
{"\r", `\r`},
@@ -185,14 +185,14 @@ func TestJSStrEscaper(t *testing.T) {
{"\\n", `\\n`},
{"foo\r\nbar", `foo\r\nbar`},
// Preserve attribute boundaries.
{`"`, `\x22`},
{`'`, `\x27`},
{`"`, `\u0022`},
{`'`, `\u0027`},
// Allow embedding in HTML without further escaping.
{`&amp;`, `\x26amp;`},
{`&amp;`, `\u0026amp;`},
// Prevent breaking out of text node and element boundaries.
{"</script>", `\x3c\/script\x3e`},
{"<![CDATA[", `\x3c![CDATA[`},
{"]]>", `]]\x3e`},
{"</script>", `\u003c\/script\u003e`},
{"<![CDATA[", `\u003c![CDATA[`},
{"]]>", `]]\u003e`},
// https://dev.w3.org/html5/markup/aria/syntax.html#escaping-text-span
// "The text in style, script, title, and textarea elements
// must not have an escaping text span start that is not
@@ -203,11 +203,11 @@ func TestJSStrEscaper(t *testing.T) {
// allow regular text content to be interpreted as script
// allowing script execution via a combination of a JS string
// injection followed by an HTML text injection.
{"<!--", `\x3c!--`},
{"-->", `--\x3e`},
{"<!--", `\u003c!--`},
{"-->", `--\u003e`},
// From https://code.google.com/p/doctype/wiki/ArticleUtf7
{"+ADw-script+AD4-alert(1)+ADw-/script+AD4-",
`\x2bADw-script\x2bAD4-alert(1)\x2bADw-\/script\x2bAD4-`,
`\u002bADw-script\u002bAD4-alert(1)\u002bADw-\/script\u002bAD4-`,
},
// Invalid UTF-8 sequence
{"foo\xA0bar", "foo\xA0bar"},
@@ -230,7 +230,7 @@ func TestJSRegexpEscaper(t *testing.T) {
}{
{"", `(?:)`},
{"foo", `foo`},
{"\u0000", `\0`},
{"\u0000", `\u0000`},
{"\t", `\t`},
{"\n", `\n`},
{"\r", `\r`},
@@ -240,19 +240,19 @@ func TestJSRegexpEscaper(t *testing.T) {
{"\\n", `\\n`},
{"foo\r\nbar", `foo\r\nbar`},
// Preserve attribute boundaries.
{`"`, `\x22`},
{`'`, `\x27`},
{`"`, `\u0022`},
{`'`, `\u0027`},
// Allow embedding in HTML without further escaping.
{`&amp;`, `\x26amp;`},
{`&amp;`, `\u0026amp;`},
// Prevent breaking out of text node and element boundaries.
{"</script>", `\x3c\/script\x3e`},
{"<![CDATA[", `\x3c!\[CDATA\[`},
{"]]>", `\]\]\x3e`},
{"</script>", `\u003c\/script\u003e`},
{"<![CDATA[", `\u003c!\[CDATA\[`},
{"]]>", `\]\]\u003e`},
// Escaping text spans.
{"<!--", `\x3c!\-\-`},
{"-->", `\-\-\x3e`},
{"<!--", `\u003c!\-\-`},
{"-->", `\-\-\u003e`},
{"*", `\*`},
{"+", `\x2b`},
{"+", `\u002b`},
{"?", `\?`},
{"[](){}", `\[\]\(\)\{\}`},
{"$foo|x.y", `\$foo\|x\.y`},
@@ -286,27 +286,27 @@ func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) {
{
"jsStrEscaper",
jsStrEscaper,
"\\0\x01\x02\x03\x04\x05\x06\x07" +
"\x08\\t\\n\\x0b\\f\\r\x0E\x0F" +
"\x10\x11\x12\x13\x14\x15\x16\x17" +
"\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
` !\x22#$%\x26\x27()*\x2b,-.\/` +
`0123456789:;\x3c=\x3e?` +
`\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007` +
`\u0008\t\n\u000b\f\r\u000e\u000f` +
`\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017` +
`\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f` +
` !\u0022#$%\u0026\u0027()*\u002b,-.\/` +
`0123456789:;\u003c=\u003e?` +
`@ABCDEFGHIJKLMNO` +
`PQRSTUVWXYZ[\\]^_` +
"`abcdefghijklmno" +
"pqrstuvwxyz{|}~\x7f" +
"pqrstuvwxyz{|}~\u007f" +
"\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
},
{
"jsRegexpEscaper",
jsRegexpEscaper,
"\\0\x01\x02\x03\x04\x05\x06\x07" +
"\x08\\t\\n\\x0b\\f\\r\x0E\x0F" +
"\x10\x11\x12\x13\x14\x15\x16\x17" +
"\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
` !\x22#\$%\x26\x27\(\)\*\x2b,\-\.\/` +
`0123456789:;\x3c=\x3e\?` +
`\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007` +
`\u0008\t\n\u000b\f\r\u000e\u000f` +
`\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017` +
`\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f` +
` !\u0022#\$%\u0026\u0027\(\)\*\u002b,\-\.\/` +
`0123456789:;\u003c=\u003e\?` +
`@ABCDEFGHIJKLMNO` +
`PQRSTUVWXYZ\[\\\]\^_` +
"`abcdefghijklmno" +

View File

@@ -8,6 +8,7 @@ package template_test
import (
"bytes"
"encoding/json"
"strings"
"testing"
@@ -124,6 +125,44 @@ func TestNumbers(t *testing.T) {
c.mustExecute(c.root, nil, "12.34 7.5")
}
func TestStringsInScriptsWithJsonContentTypeAreCorrectlyEscaped(t *testing.T) {
// See #33671 and #37634 for more context on this.
tests := []struct{ name, in string }{
{"empty", ""},
{"invalid", string(rune(-1))},
{"null", "\u0000"},
{"unit separator", "\u001F"},
{"tab", "\t"},
{"gt and lt", "<>"},
{"quotes", `'"`},
{"ASCII letters", "ASCII letters"},
{"Unicode", "ʕ⊙ϖ⊙ʔ"},
{"Pizza", "🍕"},
}
const (
prefix = `<script type="application/ld+json">`
suffix = `</script>`
templ = prefix + `"{{.}}"` + suffix
)
tpl := Must(New("JS string is JSON string").Parse(templ))
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
if err := tpl.Execute(&buf, tt.in); err != nil {
t.Fatalf("Cannot render template: %v", err)
}
trimmed := bytes.TrimSuffix(bytes.TrimPrefix(buf.Bytes(), []byte(prefix)), []byte(suffix))
var got string
if err := json.Unmarshal(trimmed, &got); err != nil {
t.Fatalf("Cannot parse JS string %q as JSON: %v", trimmed[1:len(trimmed)-1], err)
}
if got != tt.in {
t.Errorf("Serialization changed the string value: got %q want %q", got, tt.in)
}
})
}
}
type testCase struct {
t *testing.T
root *Template