]> Cypherpunks.ru repositories - gostls13.git/commitdiff
html/template,text/template: switch to Unicode escapes for JSON compatibility
authorempijei <robclap8@gmail.com>
Fri, 27 Mar 2020 18:27:55 +0000 (19:27 +0100)
committerDaniel Martí <mvdan@mvdan.cc>
Thu, 16 Apr 2020 17:13:33 +0000 (17:13 +0000)
The existing implementation is not compatible with JSON
escape as it uses hex escaping.
Unicode escape, instead, is valid for both JSON and JS.
This fix avoids creating a separate escaping context for
scripts of type "application/ld+json" and it is more
future-proof in case more JSON+JS contexts get added
to the platform (e.g. import maps).

Fixes #33671
Fixes #37634

Change-Id: Id6f6524b4abc52e81d9d744d46bbe5bf2e081543
Reviewed-on: https://go-review.googlesource.com/c/go/+/226097
Reviewed-by: Carl Johnson <me@carlmjohnson.net>
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
Run-TryBot: Daniel Martí <mvdan@mvdan.cc>
TryBot-Result: Gobot Gobot <gobot@golang.org>

src/html/template/content_test.go
src/html/template/escape_test.go
src/html/template/example_test.go
src/html/template/js.go
src/html/template/js_test.go
src/html/template/template_test.go
src/text/template/exec_test.go
src/text/template/funcs.go

index 72d56f50c14413ada8cace52f043afea819b0add..bd8652769ba4d99b927325e20aebf231c7661e8f 100644 (file)
@@ -18,7 +18,7 @@ func TestTypedContent(t *testing.T) {
                HTML(`Hello, <b>World</b> &amp;tc!`),
                HTMLAttr(` dir="ltr"`),
                JS(`c && alert("Hello, World!");`),
-               JSStr(`Hello, World & O'Reilly\x21`),
+               JSStr(`Hello, World & O'Reilly\u0021`),
                URL(`greeting=H%69,&addressee=(World)`),
                Srcset(`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`),
                URL(`,foo/,`),
@@ -70,7 +70,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/,`,
@@ -100,7 +100,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/,`,
@@ -115,7 +115,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/,`,
@@ -130,7 +130,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/,`,
@@ -146,7 +146,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/,"`,
@@ -162,7 +162,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;`,
@@ -171,30 +171,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\/,`,
                        },
                },
@@ -208,7 +208,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/,"`,
@@ -224,7 +224,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/,`,
@@ -233,15 +233,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\/,`,
                        },
                },
@@ -253,7 +253,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`,
@@ -268,7 +268,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`,
index e72a9ba11fe281bc2a2e3d82434c17bf6c442696..c709660f066a07fde41c7c1b360c64876f54f38a 100644 (file)
@@ -238,7 +238,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",
@@ -259,7 +259,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",
@@ -825,7 +825,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.
index 9d965f1943cfe95843e98485a71d26a4423e7569..6cf936f2709f33686371a54ddd5be88c438f456e 100644 (file)
@@ -116,9 +116,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 \x26 Freddie\'s Diner\" \x3Ctasty@example.com\x3E
-       // \"Fran \x26 Freddie\'s Diner\" \x3Ctasty@example.com\x3E
-       // \"Fran \x26 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
 
 }
index 0e91458d19ce934a8d1393d4e320767c7f486d64..ea9c18346ba2632b6e04db61e565492b96975156 100644 (file)
@@ -163,7 +163,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
@@ -178,8 +177,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`.
@@ -260,6 +259,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':
@@ -283,67 +284,80 @@ func replace(s string, replacementTable []string) string {
        return b.String()
 }
 
+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': `\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:    `\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`,
        '\\': `\\`,
 }
 
 // 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`,
        '?':  `\?`,
        '[':  `\[`,
        '\\': `\\`,
index 075adaafd3753c73d218ec8908e3d34fb0cb96dd..d7ee47b87d4b7b4adf3a19b1f7708a56501f47fd 100644 (file)
@@ -137,7 +137,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{}{}, "[]"},
@@ -173,7 +173,7 @@ func TestJSStrEscaper(t *testing.T) {
        }{
                {"", ``},
                {"foo", `foo`},
-               {"\u0000", `\0`},
+               {"\u0000", `\u0000`},
                {"\t", `\t`},
                {"\n", `\n`},
                {"\r", `\r`},
@@ -183,14 +183,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
@@ -201,11 +201,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"},
@@ -228,7 +228,7 @@ func TestJSRegexpEscaper(t *testing.T) {
        }{
                {"", `(?:)`},
                {"foo", `foo`},
-               {"\u0000", `\0`},
+               {"\u0000", `\u0000`},
                {"\t", `\t`},
                {"\n", `\n`},
                {"\r", `\r`},
@@ -238,19 +238,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`},
@@ -284,27 +284,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" +
index 13e6ba406e12ef92130c8bfdb98661b25ef6f844..86bd4db444d80ba53955121ae43f3f70b3a47cb1 100644 (file)
@@ -6,6 +6,7 @@ package template_test
 
 import (
        "bytes"
+       "encoding/json"
        . "html/template"
        "strings"
        "testing"
@@ -121,6 +122,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
index 77294eda4bc56a4afab7ade9d56578aef83fb66f..b8a809eee7a8bfda4cfe129ac5526e7f54ec565e 100644 (file)
@@ -911,9 +911,9 @@ func TestJSEscaping(t *testing.T) {
                {`Go "jump" \`, `Go \"jump\" \\`},
                {`Yukihiro says "今日は世界"`, `Yukihiro says \"今日は世界\"`},
                {"unprintable \uFDFF", `unprintable \uFDFF`},
-               {`<html>`, `\x3Chtml\x3E`},
-               {`no = in attributes`, `no \x3D in attributes`},
-               {`&#x27; does not become HTML entity`, `\x26#x27; does not become HTML entity`},
+               {`<html>`, `\u003Chtml\u003E`},
+               {`no = in attributes`, `no \u003D in attributes`},
+               {`&#x27; does not become HTML entity`, `\u0026#x27; does not become HTML entity`},
        }
        for _, tc := range testCases {
                s := JSEscapeString(tc.in)
index fb56bc3fc6220a91c69d64bfca1422801161a394..1b6940a84ae0d702f5775b8ca85efddd3b15afbb 100644 (file)
@@ -653,10 +653,10 @@ var (
        jsBackslash = []byte(`\\`)
        jsApos      = []byte(`\'`)
        jsQuot      = []byte(`\"`)
-       jsLt        = []byte(`\x3C`)
-       jsGt        = []byte(`\x3E`)
-       jsAmp       = []byte(`\x26`)
-       jsEq        = []byte(`\x3D`)
+       jsLt        = []byte(`\u003C`)
+       jsGt        = []byte(`\u003E`)
+       jsAmp       = []byte(`\u0026`)
+       jsEq        = []byte(`\u003D`)
 )
 
 // JSEscape writes to w the escaped JavaScript equivalent of the plain text data b.