Skip to content

Commit d4d2980

Browse files
empijeimvdan
authored andcommitted
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 <[email protected]> Reviewed-by: Daniel Martí <[email protected]> Run-TryBot: Daniel Martí <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 71a6718 commit d4d2980

File tree

8 files changed

+163
-110
lines changed

8 files changed

+163
-110
lines changed

src/html/template/content_test.go

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func TestTypedContent(t *testing.T) {
1818
HTML(`Hello, <b>World</b> &amp;tc!`),
1919
HTMLAttr(` dir="ltr"`),
2020
JS(`c && alert("Hello, World!");`),
21-
JSStr(`Hello, World & O'Reilly\x21`),
21+
JSStr(`Hello, World & O'Reilly\u0021`),
2222
URL(`greeting=H%69,&addressee=(World)`),
2323
Srcset(`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`),
2424
URL(`,foo/,`),
@@ -70,7 +70,7 @@ func TestTypedContent(t *testing.T) {
7070
`Hello, <b>World</b> &amp;tc!`,
7171
` dir=&#34;ltr&#34;`,
7272
`c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
73-
`Hello, World &amp; O&#39;Reilly\x21`,
73+
`Hello, World &amp; O&#39;Reilly\u0021`,
7474
`greeting=H%69,&amp;addressee=(World)`,
7575
`greeting=H%69,&amp;addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
7676
`,foo/,`,
@@ -100,7 +100,7 @@ func TestTypedContent(t *testing.T) {
100100
`Hello,&#32;World&#32;&amp;tc!`,
101101
`&#32;dir&#61;&#34;ltr&#34;`,
102102
`c&#32;&amp;&amp;&#32;alert(&#34;Hello,&#32;World!&#34;);`,
103-
`Hello,&#32;World&#32;&amp;&#32;O&#39;Reilly\x21`,
103+
`Hello,&#32;World&#32;&amp;&#32;O&#39;Reilly\u0021`,
104104
`greeting&#61;H%69,&amp;addressee&#61;(World)`,
105105
`greeting&#61;H%69,&amp;addressee&#61;(World)&#32;2x,&#32;https://golang.org/favicon.ico&#32;500.5w`,
106106
`,foo/,`,
@@ -115,7 +115,7 @@ func TestTypedContent(t *testing.T) {
115115
`Hello, World &amp;tc!`,
116116
` dir=&#34;ltr&#34;`,
117117
`c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
118-
`Hello, World &amp; O&#39;Reilly\x21`,
118+
`Hello, World &amp; O&#39;Reilly\u0021`,
119119
`greeting=H%69,&amp;addressee=(World)`,
120120
`greeting=H%69,&amp;addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
121121
`,foo/,`,
@@ -130,7 +130,7 @@ func TestTypedContent(t *testing.T) {
130130
`Hello, &lt;b&gt;World&lt;/b&gt; &amp;tc!`,
131131
` dir=&#34;ltr&#34;`,
132132
`c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
133-
`Hello, World &amp; O&#39;Reilly\x21`,
133+
`Hello, World &amp; O&#39;Reilly\u0021`,
134134
`greeting=H%69,&amp;addressee=(World)`,
135135
`greeting=H%69,&amp;addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
136136
`,foo/,`,
@@ -146,7 +146,7 @@ func TestTypedContent(t *testing.T) {
146146
// Not escaped.
147147
`c && alert("Hello, World!");`,
148148
// Escape sequence not over-escaped.
149-
`"Hello, World & O'Reilly\x21"`,
149+
`"Hello, World & O'Reilly\u0021"`,
150150
`"greeting=H%69,\u0026addressee=(World)"`,
151151
`"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`,
152152
`",foo/,"`,
@@ -162,7 +162,7 @@ func TestTypedContent(t *testing.T) {
162162
// Not JS escaped but HTML escaped.
163163
`c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
164164
// Escape sequence not over-escaped.
165-
`&#34;Hello, World &amp; O&#39;Reilly\x21&#34;`,
165+
`&#34;Hello, World &amp; O&#39;Reilly\u0021&#34;`,
166166
`&#34;greeting=H%69,\u0026addressee=(World)&#34;`,
167167
`&#34;greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w&#34;`,
168168
`&#34;,foo/,&#34;`,
@@ -171,30 +171,30 @@ func TestTypedContent(t *testing.T) {
171171
{
172172
`<script>alert("{{.}}")</script>`,
173173
[]string{
174-
`\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`,
175-
`a[href =~ \x22\/\/example.com\x22]#foo`,
176-
`Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`,
177-
` dir=\x22ltr\x22`,
178-
`c \x26\x26 alert(\x22Hello, World!\x22);`,
174+
`\u003cb\u003e \u0022foo%\u0022 O\u0027Reilly \u0026bar;`,
175+
`a[href =~ \u0022\/\/example.com\u0022]#foo`,
176+
`Hello, \u003cb\u003eWorld\u003c\/b\u003e \u0026amp;tc!`,
177+
` dir=\u0022ltr\u0022`,
178+
`c \u0026\u0026 alert(\u0022Hello, World!\u0022);`,
179179
// Escape sequence not over-escaped.
180-
`Hello, World \x26 O\x27Reilly\x21`,
181-
`greeting=H%69,\x26addressee=(World)`,
182-
`greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
180+
`Hello, World \u0026 O\u0027Reilly\u0021`,
181+
`greeting=H%69,\u0026addressee=(World)`,
182+
`greeting=H%69,\u0026addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
183183
`,foo\/,`,
184184
},
185185
},
186186
{
187187
`<script type="text/javascript">alert("{{.}}")</script>`,
188188
[]string{
189-
`\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`,
190-
`a[href =~ \x22\/\/example.com\x22]#foo`,
191-
`Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`,
192-
` dir=\x22ltr\x22`,
193-
`c \x26\x26 alert(\x22Hello, World!\x22);`,
189+
`\u003cb\u003e \u0022foo%\u0022 O\u0027Reilly \u0026bar;`,
190+
`a[href =~ \u0022\/\/example.com\u0022]#foo`,
191+
`Hello, \u003cb\u003eWorld\u003c\/b\u003e \u0026amp;tc!`,
192+
` dir=\u0022ltr\u0022`,
193+
`c \u0026\u0026 alert(\u0022Hello, World!\u0022);`,
194194
// Escape sequence not over-escaped.
195-
`Hello, World \x26 O\x27Reilly\x21`,
196-
`greeting=H%69,\x26addressee=(World)`,
197-
`greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
195+
`Hello, World \u0026 O\u0027Reilly\u0021`,
196+
`greeting=H%69,\u0026addressee=(World)`,
197+
`greeting=H%69,\u0026addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
198198
`,foo\/,`,
199199
},
200200
},
@@ -208,7 +208,7 @@ func TestTypedContent(t *testing.T) {
208208
// Not escaped.
209209
`c && alert("Hello, World!");`,
210210
// Escape sequence not over-escaped.
211-
`"Hello, World & O'Reilly\x21"`,
211+
`"Hello, World & O'Reilly\u0021"`,
212212
`"greeting=H%69,\u0026addressee=(World)"`,
213213
`"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`,
214214
`",foo/,"`,
@@ -224,7 +224,7 @@ func TestTypedContent(t *testing.T) {
224224
`Hello, <b>World</b> &amp;tc!`,
225225
` dir=&#34;ltr&#34;`,
226226
`c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
227-
`Hello, World &amp; O&#39;Reilly\x21`,
227+
`Hello, World &amp; O&#39;Reilly\u0021`,
228228
`greeting=H%69,&amp;addressee=(World)`,
229229
`greeting=H%69,&amp;addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
230230
`,foo/,`,
@@ -233,15 +233,15 @@ func TestTypedContent(t *testing.T) {
233233
{
234234
`<button onclick='alert("{{.}}")'>`,
235235
[]string{
236-
`\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`,
237-
`a[href =~ \x22\/\/example.com\x22]#foo`,
238-
`Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`,
239-
` dir=\x22ltr\x22`,
240-
`c \x26\x26 alert(\x22Hello, World!\x22);`,
236+
`\u003cb\u003e \u0022foo%\u0022 O\u0027Reilly \u0026bar;`,
237+
`a[href =~ \u0022\/\/example.com\u0022]#foo`,
238+
`Hello, \u003cb\u003eWorld\u003c\/b\u003e \u0026amp;tc!`,
239+
` dir=\u0022ltr\u0022`,
240+
`c \u0026\u0026 alert(\u0022Hello, World!\u0022);`,
241241
// Escape sequence not over-escaped.
242-
`Hello, World \x26 O\x27Reilly\x21`,
243-
`greeting=H%69,\x26addressee=(World)`,
244-
`greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
242+
`Hello, World \u0026 O\u0027Reilly\u0021`,
243+
`greeting=H%69,\u0026addressee=(World)`,
244+
`greeting=H%69,\u0026addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`,
245245
`,foo\/,`,
246246
},
247247
},
@@ -253,7 +253,7 @@ func TestTypedContent(t *testing.T) {
253253
`Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21`,
254254
`%20dir%3d%22ltr%22`,
255255
`c%20%26%26%20alert%28%22Hello%2c%20World%21%22%29%3b`,
256-
`Hello%2c%20World%20%26%20O%27Reilly%5cx21`,
256+
`Hello%2c%20World%20%26%20O%27Reilly%5cu0021`,
257257
// Quotes and parens are escaped but %69 is not over-escaped. HTML escaping is done.
258258
`greeting=H%69,&amp;addressee=%28World%29`,
259259
`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) {
268268
`Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21`,
269269
`%20dir%3d%22ltr%22`,
270270
`c%20%26%26%20alert%28%22Hello%2c%20World%21%22%29%3b`,
271-
`Hello%2c%20World%20%26%20O%27Reilly%5cx21`,
271+
`Hello%2c%20World%20%26%20O%27Reilly%5cu0021`,
272272
// Quotes and parens are escaped but %69 is not over-escaped. HTML escaping is not done.
273273
`greeting=H%69,&addressee=%28World%29`,
274274
`greeting%3dH%2569%2c%26addressee%3d%28World%29%202x%2c%20https%3a%2f%2fgolang.org%2ffavicon.ico%20500.5w`,

src/html/template/escape_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ func TestEscape(t *testing.T) {
238238
{
239239
"jsStr",
240240
"<button onclick='alert(&quot;{{.H}}&quot;)'>",
241-
`<button onclick='alert(&quot;\x3cHello\x3e&quot;)'>`,
241+
`<button onclick='alert(&quot;\u003cHello\u003e&quot;)'>`,
242242
},
243243
{
244244
"badMarshaler",
@@ -259,7 +259,7 @@ func TestEscape(t *testing.T) {
259259
{
260260
"jsRe",
261261
`<button onclick='alert(/{{"foo+bar"}}/.test(""))'>`,
262-
`<button onclick='alert(/foo\x2bbar/.test(""))'>`,
262+
`<button onclick='alert(/foo\u002bbar/.test(""))'>`,
263263
},
264264
{
265265
"jsReBlank",
@@ -825,7 +825,7 @@ func TestEscapeSet(t *testing.T) {
825825
"main": `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`,
826826
"helper": `{{11}} of {{"<100>"}}`,
827827
},
828-
`<button onclick="title='11 of \x3c100\x3e'; ...">11 of &lt;100&gt;</button>`,
828+
`<button onclick="title='11 of \u003c100\u003e'; ...">11 of &lt;100&gt;</button>`,
829829
},
830830
// A non-recursive template that ends in a different context.
831831
// helper starts in jsCtxRegexp and ends in jsCtxDivOp.

src/html/template/example_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,9 @@ func Example_escape() {
116116
// &#34;Fran &amp; Freddie&#39;s Diner&#34; &lt;[email protected]&gt;
117117
// &#34;Fran &amp; Freddie&#39;s Diner&#34; &lt;[email protected]&gt;
118118
// &#34;Fran &amp; Freddie&#39;s Diner&#34;32&lt;[email protected]&gt;
119-
// \"Fran \x26 Freddie\'s Diner\" \x3Ctasty@example.com\x3E
120-
// \"Fran \x26 Freddie\'s Diner\" \x3Ctasty@example.com\x3E
121-
// \"Fran \x26 Freddie\'s Diner\"32\x3Ctasty@example.com\x3E
119+
// \"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
120+
// \"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
121+
// \"Fran \u0026 Freddie\'s Diner\"32\u003Ctasty@example.com\u003E
122122
// %22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E
123123

124124
}

src/html/template/js.go

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,6 @@ func jsValEscaper(args ...interface{}) string {
163163
}
164164
// TODO: detect cycles before calling Marshal which loops infinitely on
165165
// cyclic data. This may be an unacceptable DoS risk.
166-
167166
b, err := json.Marshal(a)
168167
if err != nil {
169168
// Put a space before comment so that if it is flush against
@@ -178,8 +177,8 @@ func jsValEscaper(args ...interface{}) string {
178177
// TODO: maybe post-process output to prevent it from containing
179178
// "<!--", "-->", "<![CDATA[", "]]>", or "</script"
180179
// in case custom marshalers produce output containing those.
181-
182-
// TODO: Maybe abbreviate \u00ab to \xab to produce more compact output.
180+
// Note: Do not use \x escaping to save bytes because it is not JSON compatible and this escaper
181+
// supports ld+json content-type.
183182
if len(b) == 0 {
184183
// In, `x=y/{{.}}*z` a json.Marshaler that produces "" should
185184
// not cause the output `x=y/*z`.
@@ -260,6 +259,8 @@ func replace(s string, replacementTable []string) string {
260259
r, w = utf8.DecodeRuneInString(s[i:])
261260
var repl string
262261
switch {
262+
case int(r) < len(lowUnicodeReplacementTable):
263+
repl = lowUnicodeReplacementTable[r]
263264
case int(r) < len(replacementTable) && replacementTable[r] != "":
264265
repl = replacementTable[r]
265266
case r == '\u2028':
@@ -283,67 +284,80 @@ func replace(s string, replacementTable []string) string {
283284
return b.String()
284285
}
285286

287+
var lowUnicodeReplacementTable = []string{
288+
0: `\u0000`, 1: `\u0001`, 2: `\u0002`, 3: `\u0003`, 4: `\u0004`, 5: `\u0005`, 6: `\u0006`,
289+
'\a': `\u0007`,
290+
'\b': `\u0008`,
291+
'\t': `\t`,
292+
'\n': `\n`,
293+
'\v': `\u000b`, // "\v" == "v" on IE 6.
294+
'\f': `\f`,
295+
'\r': `\r`,
296+
0xe: `\u000e`, 0xf: `\u000f`, 0x10: `\u0010`, 0x11: `\u0011`, 0x12: `\u0012`, 0x13: `\u0013`,
297+
0x14: `\u0014`, 0x15: `\u0015`, 0x16: `\u0016`, 0x17: `\u0017`, 0x18: `\u0018`, 0x19: `\u0019`,
298+
0x1a: `\u001a`, 0x1b: `\u001b`, 0x1c: `\u001c`, 0x1d: `\u001d`, 0x1e: `\u001e`, 0x1f: `\u001f`,
299+
}
300+
286301
var jsStrReplacementTable = []string{
287-
0: `\0`,
302+
0: `\u0000`,
288303
'\t': `\t`,
289304
'\n': `\n`,
290-
'\v': `\x0b`, // "\v" == "v" on IE 6.
305+
'\v': `\u000b`, // "\v" == "v" on IE 6.
291306
'\f': `\f`,
292307
'\r': `\r`,
293308
// Encode HTML specials as hex so the output can be embedded
294309
// in HTML attributes without further encoding.
295-
'"': `\x22`,
296-
'&': `\x26`,
297-
'\'': `\x27`,
298-
'+': `\x2b`,
310+
'"': `\u0022`,
311+
'&': `\u0026`,
312+
'\'': `\u0027`,
313+
'+': `\u002b`,
299314
'/': `\/`,
300-
'<': `\x3c`,
301-
'>': `\x3e`,
315+
'<': `\u003c`,
316+
'>': `\u003e`,
302317
'\\': `\\`,
303318
}
304319

305320
// jsStrNormReplacementTable is like jsStrReplacementTable but does not
306321
// overencode existing escapes since this table has no entry for `\`.
307322
var jsStrNormReplacementTable = []string{
308-
0: `\0`,
323+
0: `\u0000`,
309324
'\t': `\t`,
310325
'\n': `\n`,
311-
'\v': `\x0b`, // "\v" == "v" on IE 6.
326+
'\v': `\u000b`, // "\v" == "v" on IE 6.
312327
'\f': `\f`,
313328
'\r': `\r`,
314329
// Encode HTML specials as hex so the output can be embedded
315330
// in HTML attributes without further encoding.
316-
'"': `\x22`,
317-
'&': `\x26`,
318-
'\'': `\x27`,
319-
'+': `\x2b`,
331+
'"': `\u0022`,
332+
'&': `\u0026`,
333+
'\'': `\u0027`,
334+
'+': `\u002b`,
320335
'/': `\/`,
321-
'<': `\x3c`,
322-
'>': `\x3e`,
336+
'<': `\u003c`,
337+
'>': `\u003e`,
323338
}
324-
325339
var jsRegexpReplacementTable = []string{
326-
0: `\0`,
340+
0: `\u0000`,
327341
'\t': `\t`,
328342
'\n': `\n`,
329-
'\v': `\x0b`, // "\v" == "v" on IE 6.
343+
'\v': `\u000b`, // "\v" == "v" on IE 6.
330344
'\f': `\f`,
331345
'\r': `\r`,
332346
// Encode HTML specials as hex so the output can be embedded
333347
// in HTML attributes without further encoding.
334-
'"': `\x22`,
348+
'"': `\u0022`,
335349
'$': `\$`,
336-
'&': `\x26`,
337-
'\'': `\x27`,
350+
'&': `\u0026`,
351+
'\'': `\u0027`,
338352
'(': `\(`,
339353
')': `\)`,
340354
'*': `\*`,
341-
'+': `\x2b`,
355+
'+': `\u002b`,
342356
'-': `\-`,
343357
'.': `\.`,
344358
'/': `\/`,
345-
'<': `\x3c`,
346-
'>': `\x3e`,
359+
'<': `\u003c`,
360+
'>': `\u003e`,
347361
'?': `\?`,
348362
'[': `\[`,
349363
'\\': `\\`,

0 commit comments

Comments
 (0)