summaryrefslogtreecommitdiff
path: root/src/html
diff options
context:
space:
mode:
authorempijei <robclap8@gmail.com>2020-03-27 19:27:55 +0100
committerDaniel Martí <mvdan@mvdan.cc>2020-04-16 17:13:33 +0000
commitd4d298040d072ddacea0e0d6b55fb148fff18070 (patch)
treeb36c1068e498b09f0e1b320c2784ba14ddf4ec8b /src/html
parent71a671839f95fb43091316c72cae87c049c81bce (diff)
downloadgo-git-d4d298040d072ddacea0e0d6b55fb148fff18070.tar.gz
html/template,text/template: switch to Unicode escapes for JSON compatibility
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>
Diffstat (limited to 'src/html')
-rw-r--r--src/html/template/content_test.go70
-rw-r--r--src/html/template/escape_test.go6
-rw-r--r--src/html/template/example_test.go6
-rw-r--r--src/html/template/js.go70
-rw-r--r--src/html/template/js_test.go68
-rw-r--r--src/html/template/template_test.go39
6 files changed, 156 insertions, 103 deletions
diff --git a/src/html/template/content_test.go b/src/html/template/content_test.go
index 72d56f50c1..bd8652769b 100644
--- a/src/html/template/content_test.go
+++ b/src/html/template/content_test.go
@@ -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`,
diff --git a/src/html/template/escape_test.go b/src/html/template/escape_test.go
index e72a9ba11f..c709660f06 100644
--- a/src/html/template/escape_test.go
+++ b/src/html/template/escape_test.go
@@ -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.
diff --git a/src/html/template/example_test.go b/src/html/template/example_test.go
index 9d965f1943..6cf936f270 100644
--- a/src/html/template/example_test.go
+++ b/src/html/template/example_test.go
@@ -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
}
diff --git a/src/html/template/js.go b/src/html/template/js.go
index 0e91458d19..ea9c18346b 100644
--- a/src/html/template/js.go
+++ b/src/html/template/js.go
@@ -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`,
'?': `\?`,
'[': `\[`,
'\\': `\\`,
diff --git a/src/html/template/js_test.go b/src/html/template/js_test.go
index 075adaafd3..d7ee47b87d 100644
--- a/src/html/template/js_test.go
+++ b/src/html/template/js_test.go
@@ -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" +
diff --git a/src/html/template/template_test.go b/src/html/template/template_test.go
index 13e6ba406e..86bd4db444 100644
--- a/src/html/template/template_test.go
+++ b/src/html/template/template_test.go
@@ -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