1
2
3
4
5 package template
6
7 import (
8 "bytes"
9 "encoding/json"
10 "fmt"
11 "internal/testenv"
12 "os"
13 "strings"
14 "testing"
15 "text/template"
16 "text/template/parse"
17 )
18
19 type badMarshaler struct{}
20
21 func (x *badMarshaler) MarshalJSON() ([]byte, error) {
22
23 return []byte("{ foo: 'not quite valid JSON' }"), nil
24 }
25
26 type goodMarshaler struct{}
27
28 func (x *goodMarshaler) MarshalJSON() ([]byte, error) {
29 return []byte(`{ "<foo>": "O'Reilly" }`), nil
30 }
31
32 func TestEscape(t *testing.T) {
33 data := struct {
34 F, T bool
35 C, G, H, I string
36 A, E []string
37 B, M json.Marshaler
38 N int
39 U any
40 Z *int
41 W HTML
42 }{
43 F: false,
44 T: true,
45 C: "<Cincinnati>",
46 G: "<Goodbye>",
47 H: "<Hello>",
48 A: []string{"<a>", "<b>"},
49 E: []string{},
50 N: 42,
51 B: &badMarshaler{},
52 M: &goodMarshaler{},
53 U: nil,
54 Z: nil,
55 W: HTML(`¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`),
56 I: "${ asd `` }",
57 }
58 pdata := &data
59
60 tests := []struct {
61 name string
62 input string
63 output string
64 }{
65 {
66 "if",
67 "{{if .T}}Hello{{end}}, {{.C}}!",
68 "Hello, <Cincinnati>!",
69 },
70 {
71 "else",
72 "{{if .F}}{{.H}}{{else}}{{.G}}{{end}}!",
73 "<Goodbye>!",
74 },
75 {
76 "overescaping1",
77 "Hello, {{.C | html}}!",
78 "Hello, <Cincinnati>!",
79 },
80 {
81 "overescaping2",
82 "Hello, {{html .C}}!",
83 "Hello, <Cincinnati>!",
84 },
85 {
86 "overescaping3",
87 "{{with .C}}{{$msg := .}}Hello, {{$msg}}!{{end}}",
88 "Hello, <Cincinnati>!",
89 },
90 {
91 "assignment",
92 "{{if $x := .H}}{{$x}}{{end}}",
93 "<Hello>",
94 },
95 {
96 "withBody",
97 "{{with .H}}{{.}}{{end}}",
98 "<Hello>",
99 },
100 {
101 "withElse",
102 "{{with .E}}{{.}}{{else}}{{.H}}{{end}}",
103 "<Hello>",
104 },
105 {
106 "rangeBody",
107 "{{range .A}}{{.}}{{end}}",
108 "<a><b>",
109 },
110 {
111 "rangeElse",
112 "{{range .E}}{{.}}{{else}}{{.H}}{{end}}",
113 "<Hello>",
114 },
115 {
116 "nonStringValue",
117 "{{.T}}",
118 "true",
119 },
120 {
121 "untypedNilValue",
122 "{{.U}}",
123 "",
124 },
125 {
126 "typedNilValue",
127 "{{.Z}}",
128 "<nil>",
129 },
130 {
131 "constant",
132 `<a href="/search?q={{"'a<b'"}}">`,
133 `<a href="/search?q=%27a%3cb%27">`,
134 },
135 {
136 "multipleAttrs",
137 "<a b=1 c={{.H}}>",
138 "<a b=1 c=<Hello>>",
139 },
140 {
141 "urlStartRel",
142 `<a href='{{"/foo/bar?a=b&c=d"}}'>`,
143 `<a href='/foo/bar?a=b&c=d'>`,
144 },
145 {
146 "urlStartAbsOk",
147 `<a href='{{"http://example.com/foo/bar?a=b&c=d"}}'>`,
148 `<a href='http://example.com/foo/bar?a=b&c=d'>`,
149 },
150 {
151 "protocolRelativeURLStart",
152 `<a href='{{"//example.com:8000/foo/bar?a=b&c=d"}}'>`,
153 `<a href='//example.com:8000/foo/bar?a=b&c=d'>`,
154 },
155 {
156 "pathRelativeURLStart",
157 `<a href="{{"/javascript:80/foo/bar"}}">`,
158 `<a href="/javascript:80/foo/bar">`,
159 },
160 {
161 "dangerousURLStart",
162 `<a href='{{"javascript:alert(%22pwned%22)"}}'>`,
163 `<a href='#ZgotmplZ'>`,
164 },
165 {
166 "dangerousURLStart2",
167 `<a href=' {{"javascript:alert(%22pwned%22)"}}'>`,
168 `<a href=' #ZgotmplZ'>`,
169 },
170 {
171 "nonHierURL",
172 `<a href={{"mailto:Muhammed \"The Greatest\" Ali <m.ali@example.com>"}}>`,
173 `<a href=mailto:Muhammed%20%22The%20Greatest%22%20Ali%20%3cm.ali@example.com%3e>`,
174 },
175 {
176 "urlPath",
177 `<a href='http://{{"javascript:80"}}/foo'>`,
178 `<a href='http://javascript:80/foo'>`,
179 },
180 {
181 "urlQuery",
182 `<a href='/search?q={{.H}}'>`,
183 `<a href='/search?q=%3cHello%3e'>`,
184 },
185 {
186 "urlFragment",
187 `<a href='/faq#{{.H}}'>`,
188 `<a href='/faq#%3cHello%3e'>`,
189 },
190 {
191 "urlBranch",
192 `<a href="{{if .F}}/foo?a=b{{else}}/bar{{end}}">`,
193 `<a href="/bar">`,
194 },
195 {
196 "urlBranchConflictMoot",
197 `<a href="{{if .T}}/foo?a={{else}}/bar#{{end}}{{.C}}">`,
198 `<a href="/foo?a=%3cCincinnati%3e">`,
199 },
200 {
201 "jsStrValue",
202 "<button onclick='alert({{.H}})'>",
203 `<button onclick='alert("\u003cHello\u003e")'>`,
204 },
205 {
206 "jsNumericValue",
207 "<button onclick='alert({{.N}})'>",
208 `<button onclick='alert( 42 )'>`,
209 },
210 {
211 "jsBoolValue",
212 "<button onclick='alert({{.T}})'>",
213 `<button onclick='alert( true )'>`,
214 },
215 {
216 "jsNilValueTyped",
217 "<button onclick='alert(typeof{{.Z}})'>",
218 `<button onclick='alert(typeof null )'>`,
219 },
220 {
221 "jsNilValueUntyped",
222 "<button onclick='alert(typeof{{.U}})'>",
223 `<button onclick='alert(typeof null )'>`,
224 },
225 {
226 "jsObjValue",
227 "<button onclick='alert({{.A}})'>",
228 `<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`,
229 },
230 {
231 "jsObjValueScript",
232 "<script>alert({{.A}})</script>",
233 `<script>alert(["\u003ca\u003e","\u003cb\u003e"])</script>`,
234 },
235 {
236 "scriptTypeSpace",
237 "<script type=\" \">{{.H}}</script>",
238 "<script type=\" \">\"\\u003cHello\\u003e\"</script>",
239 },
240 {
241 "scriptTypeTab",
242 "<script type=\"\t\">{{.H}}</script>",
243 "<script type=\"\t\">\"\\u003cHello\\u003e\"</script>",
244 },
245 {
246 "scriptTypeEmpty",
247 "<script type=\"\">{{.H}}</script>",
248 "<script type=\"\">\"\\u003cHello\\u003e\"</script>",
249 },
250 {
251 "jsObjValueNotOverEscaped",
252 "<button onclick='alert({{.A | html}})'>",
253 `<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`,
254 },
255 {
256 "jsStr",
257 "<button onclick='alert("{{.H}}")'>",
258 `<button onclick='alert("\u003cHello\u003e")'>`,
259 },
260 {
261 "badMarshaler",
262 `<button onclick='alert(1/{{.B}}in numbers)'>`,
263 `<button onclick='alert(1/ /* json: error calling MarshalJSON for type *template.badMarshaler: invalid character 'f' looking for beginning of object key string */null in numbers)'>`,
264 },
265 {
266 "jsMarshaler",
267 `<button onclick='alert({{.M}})'>`,
268 `<button onclick='alert({"\u003cfoo\u003e":"O'Reilly"})'>`,
269 },
270 {
271 "jsStrNotUnderEscaped",
272 "<button onclick='alert({{.C | urlquery}})'>",
273
274 `<button onclick='alert("%3CCincinnati%3E")'>`,
275 },
276 {
277 "jsRe",
278 `<button onclick='alert(/{{"foo+bar"}}/.test(""))'>`,
279 `<button onclick='alert(/foo\u002bbar/.test(""))'>`,
280 },
281 {
282 "jsReBlank",
283 `<script>alert(/{{""}}/.test(""));</script>`,
284 `<script>alert(/(?:)/.test(""));</script>`,
285 },
286 {
287 "jsReAmbigOk",
288 `<script>{{if true}}var x = 1{{end}}</script>`,
289
290
291 `<script>var x = 1</script>`,
292 },
293 {
294 "styleBidiKeywordPassed",
295 `<p style="dir: {{"ltr"}}">`,
296 `<p style="dir: ltr">`,
297 },
298 {
299 "styleBidiPropNamePassed",
300 `<p style="border-{{"left"}}: 0; border-{{"right"}}: 1in">`,
301 `<p style="border-left: 0; border-right: 1in">`,
302 },
303 {
304 "styleExpressionBlocked",
305 `<p style="width: {{"expression(alert(1337))"}}">`,
306 `<p style="width: ZgotmplZ">`,
307 },
308 {
309 "styleTagSelectorPassed",
310 `<style>{{"p"}} { color: pink }</style>`,
311 `<style>p { color: pink }</style>`,
312 },
313 {
314 "styleIDPassed",
315 `<style>p{{"#my-ID"}} { font: Arial }</style>`,
316 `<style>p#my-ID { font: Arial }</style>`,
317 },
318 {
319 "styleClassPassed",
320 `<style>p{{".my_class"}} { font: Arial }</style>`,
321 `<style>p.my_class { font: Arial }</style>`,
322 },
323 {
324 "styleQuantityPassed",
325 `<a style="left: {{"2em"}}; top: {{0}}">`,
326 `<a style="left: 2em; top: 0">`,
327 },
328 {
329 "stylePctPassed",
330 `<table style=width:{{"100%"}}>`,
331 `<table style=width:100%>`,
332 },
333 {
334 "styleColorPassed",
335 `<p style="color: {{"#8ff"}}; background: {{"#000"}}">`,
336 `<p style="color: #8ff; background: #000">`,
337 },
338 {
339 "styleObfuscatedExpressionBlocked",
340 `<p style="width: {{" e\\78preS\x00Sio/**/n(alert(1337))"}}">`,
341 `<p style="width: ZgotmplZ">`,
342 },
343 {
344 "styleMozBindingBlocked",
345 `<p style="{{"-moz-binding(alert(1337))"}}: ...">`,
346 `<p style="ZgotmplZ: ...">`,
347 },
348 {
349 "styleObfuscatedMozBindingBlocked",
350 `<p style="{{" -mo\\7a-B\x00I/**/nding(alert(1337))"}}: ...">`,
351 `<p style="ZgotmplZ: ...">`,
352 },
353 {
354 "styleFontNameString",
355 `<p style='font-family: "{{"Times New Roman"}}"'>`,
356 `<p style='font-family: "Times New Roman"'>`,
357 },
358 {
359 "styleFontNameString",
360 `<p style='font-family: "{{"Times New Roman"}}", "{{"sans-serif"}}"'>`,
361 `<p style='font-family: "Times New Roman", "sans-serif"'>`,
362 },
363 {
364 "styleFontNameUnquoted",
365 `<p style='font-family: {{"Times New Roman"}}'>`,
366 `<p style='font-family: Times New Roman'>`,
367 },
368 {
369 "styleURLQueryEncoded",
370 `<p style="background: url(/img?name={{"O'Reilly Animal(1)<2>.png"}})">`,
371 `<p style="background: url(/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png)">`,
372 },
373 {
374 "styleQuotedURLQueryEncoded",
375 `<p style="background: url('/img?name={{"O'Reilly Animal(1)<2>.png"}}')">`,
376 `<p style="background: url('/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png')">`,
377 },
378 {
379 "styleStrQueryEncoded",
380 `<p style="background: '/img?name={{"O'Reilly Animal(1)<2>.png"}}'">`,
381 `<p style="background: '/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png'">`,
382 },
383 {
384 "styleURLBadProtocolBlocked",
385 `<a style="background: url('{{"javascript:alert(1337)"}}')">`,
386 `<a style="background: url('#ZgotmplZ')">`,
387 },
388 {
389 "styleStrBadProtocolBlocked",
390 `<a style="background: '{{"vbscript:alert(1337)"}}'">`,
391 `<a style="background: '#ZgotmplZ'">`,
392 },
393 {
394 "styleStrEncodedProtocolEncoded",
395 `<a style="background: '{{"javascript\\3a alert(1337)"}}'">`,
396
397 `<a style="background: 'javascript\\3a alert\28 1337\29 '">`,
398 },
399 {
400 "styleURLGoodProtocolPassed",
401 `<a style="background: url('{{"http://oreilly.com/O'Reilly Animals(1)<2>;{}.html"}}')">`,
402 `<a style="background: url('http://oreilly.com/O%27Reilly%20Animals%281%29%3c2%3e;%7b%7d.html')">`,
403 },
404 {
405 "styleStrGoodProtocolPassed",
406 `<a style="background: '{{"http://oreilly.com/O'Reilly Animals(1)<2>;{}.html"}}'">`,
407 `<a style="background: 'http\3a\2f\2foreilly.com\2fO\27Reilly Animals\28 1\29\3c 2\3e\3b\7b\7d.html'">`,
408 },
409 {
410 "styleURLEncodedForHTMLInAttr",
411 `<a style="background: url('{{"/search?img=foo&size=icon"}}')">`,
412 `<a style="background: url('/search?img=foo&size=icon')">`,
413 },
414 {
415 "styleURLNotEncodedForHTMLInCdata",
416 `<style>body { background: url('{{"/search?img=foo&size=icon"}}') }</style>`,
417 `<style>body { background: url('/search?img=foo&size=icon') }</style>`,
418 },
419 {
420 "styleURLMixedCase",
421 `<p style="background: URL(#{{.H}})">`,
422 `<p style="background: URL(#%3cHello%3e)">`,
423 },
424 {
425 "stylePropertyPairPassed",
426 `<a style='{{"color: red"}}'>`,
427 `<a style='color: red'>`,
428 },
429 {
430 "styleStrSpecialsEncoded",
431 `<a style="font-family: '{{"/**/'\";:// \\"}}', "{{"/**/'\";:// \\"}}"">`,
432 `<a style="font-family: '\2f**\2f\27\22\3b\3a\2f\2f \\', "\2f**\2f\27\22\3b\3a\2f\2f \\"">`,
433 },
434 {
435 "styleURLSpecialsEncoded",
436 `<a style="border-image: url({{"/**/'\";:// \\"}}), url("{{"/**/'\";:// \\"}}"), url('{{"/**/'\";:// \\"}}'), 'http://www.example.com/?q={{"/**/'\";:// \\"}}''">`,
437 `<a style="border-image: url(/**/%27%22;://%20%5c), url("/**/%27%22;://%20%5c"), url('/**/%27%22;://%20%5c'), 'http://www.example.com/?q=%2f%2a%2a%2f%27%22%3b%3a%2f%2f%20%5c''">`,
438 },
439 {
440 "HTML comment",
441 "<b>Hello, <!-- name of world -->{{.C}}</b>",
442 "<b>Hello, <Cincinnati></b>",
443 },
444 {
445 "HTML comment not first < in text node.",
446 "<<!-- -->!--",
447 "<!--",
448 },
449 {
450 "HTML normalization 1",
451 "a < b",
452 "a < b",
453 },
454 {
455 "HTML normalization 2",
456 "a << b",
457 "a << b",
458 },
459 {
460 "HTML normalization 3",
461 "a<<!-- --><!-- -->b",
462 "a<b",
463 },
464 {
465 "HTML doctype not normalized",
466 "<!DOCTYPE html>Hello, World!",
467 "<!DOCTYPE html>Hello, World!",
468 },
469 {
470 "HTML doctype not case-insensitive",
471 "<!doCtYPE htMl>Hello, World!",
472 "<!doCtYPE htMl>Hello, World!",
473 },
474 {
475 "No doctype injection",
476 `<!{{"DOCTYPE"}}`,
477 "<!DOCTYPE",
478 },
479 {
480 "Split HTML comment",
481 "<b>Hello, <!-- name of {{if .T}}city -->{{.C}}{{else}}world -->{{.W}}{{end}}</b>",
482 "<b>Hello, <Cincinnati></b>",
483 },
484 {
485 "JS line comment",
486 "<script>for (;;) { if (c()) break// foo not a label\n" +
487 "foo({{.T}});}</script>",
488 "<script>for (;;) { if (c()) break\n" +
489 "foo( true );}</script>",
490 },
491 {
492 "JS multiline block comment",
493 "<script>for (;;) { if (c()) break/* foo not a label\n" +
494 " */foo({{.T}});}</script>",
495
496
497
498 "<script>for (;;) { if (c()) break\n" +
499 "foo( true );}</script>",
500 },
501 {
502 "JS single-line block comment",
503 "<script>for (;;) {\n" +
504 "if (c()) break/* foo a label */foo;" +
505 "x({{.T}});}</script>",
506
507
508
509 "<script>for (;;) {\n" +
510 "if (c()) break foo;" +
511 "x( true );}</script>",
512 },
513 {
514 "JS block comment flush with mathematical division",
515 "<script>var a/*b*//c\nd</script>",
516 "<script>var a /c\nd</script>",
517 },
518 {
519 "JS mixed comments",
520 "<script>var a/*b*///c\nd</script>",
521 "<script>var a \nd</script>",
522 },
523 {
524 "JS HTML-like comments",
525 "<script>before <!-- beep\nbetween\nbefore-->boop\n</script>",
526 "<script>before \nbetween\nbefore\n</script>",
527 },
528 {
529 "JS hashbang comment",
530 "<script>#! beep\n</script>",
531 "<script>\n</script>",
532 },
533 {
534 "Special tags in <script> string literals",
535 `<script>var a = "asd < 123 <!-- 456 < fgh <script jkl < 789 </script"</script>`,
536 `<script>var a = "asd < 123 \x3C!-- 456 < fgh \x3Cscript jkl < 789 \x3C/script"</script>`,
537 },
538 {
539 "Special tags in <script> string literals (mixed case)",
540 `<script>var a = "<!-- <ScripT </ScripT"</script>`,
541 `<script>var a = "\x3C!-- \x3CScripT \x3C/ScripT"</script>`,
542 },
543 {
544 "Special tags in <script> regex literals (mixed case)",
545 `<script>var a = /<!-- <ScripT </ScripT/</script>`,
546 `<script>var a = /\x3C!-- \x3CScripT \x3C/ScripT/</script>`,
547 },
548 {
549 "CSS comments",
550 "<style>p// paragraph\n" +
551 `{border: 1px/* color */{{"#00f"}}}</style>`,
552 "<style>p\n" +
553 "{border: 1px #00f}</style>",
554 },
555 {
556 "JS attr block comment",
557 `<a onclick="f(""); /* alert({{.H}}) */">`,
558
559
560 `<a onclick="f(""); /* alert() */">`,
561 },
562 {
563 "JS attr line comment",
564 `<a onclick="// alert({{.G}})">`,
565 `<a onclick="// alert()">`,
566 },
567 {
568 "CSS attr block comment",
569 `<a style="/* color: {{.H}} */">`,
570 `<a style="/* color: */">`,
571 },
572 {
573 "CSS attr line comment",
574 `<a style="// color: {{.G}}">`,
575 `<a style="// color: ">`,
576 },
577 {
578 "HTML substitution commented out",
579 "<p><!-- {{.H}} --></p>",
580 "<p></p>",
581 },
582 {
583 "Comment ends flush with start",
584 "<!--{{.}}--><script>/*{{.}}*///{{.}}\n</script><style>/*{{.}}*///{{.}}\n</style><a onclick='/*{{.}}*///{{.}}' style='/*{{.}}*///{{.}}'>",
585 "<script> \n</script><style> \n</style><a onclick='/**///' style='/**///'>",
586 },
587 {
588 "typed HTML in text",
589 `{{.W}}`,
590 `¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`,
591 },
592 {
593 "typed HTML in attribute",
594 `<div title="{{.W}}">`,
595 `<div title="¡Hello, O'World!">`,
596 },
597 {
598 "typed HTML in script",
599 `<button onclick="alert({{.W}})">`,
600 `<button onclick="alert("\u0026iexcl;\u003cb class=\"foo\"\u003eHello\u003c/b\u003e, \u003ctextarea\u003eO'World\u003c/textarea\u003e!")">`,
601 },
602 {
603 "typed HTML in RCDATA",
604 `<textarea>{{.W}}</textarea>`,
605 `<textarea>¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!</textarea>`,
606 },
607 {
608 "range in textarea",
609 "<textarea>{{range .A}}{{.}}{{end}}</textarea>",
610 "<textarea><a><b></textarea>",
611 },
612 {
613 "No tag injection",
614 `{{"10$"}}<{{"script src,evil.org/pwnd.js"}}...`,
615 `10$<script src,evil.org/pwnd.js...`,
616 },
617 {
618 "No comment injection",
619 `<{{"!--"}}`,
620 `<!--`,
621 },
622 {
623 "No RCDATA end tag injection",
624 `<textarea><{{"/textarea "}}...</textarea>`,
625 `<textarea></textarea ...</textarea>`,
626 },
627 {
628 "optional attrs",
629 `<img class="{{"iconClass"}}"` +
630 `{{if .T}} id="{{"<iconId>"}}"{{end}}` +
631
632 ` src=` +
633 `{{if .T}}"?{{"<iconPath>"}}"` +
634 `{{else}}"images/cleardot.gif"{{end}}` +
635
636
637 `{{if .T}}title="{{"<title>"}}"{{end}}` +
638
639 ` alt="` +
640 `{{if .T}}{{"<alt>"}}` +
641 `{{else}}{{if .F}}{{"<title>"}}{{end}}` +
642 `{{end}}"` +
643 `>`,
644 `<img class="iconClass" id="<iconId>" src="?%3ciconPath%3e"title="<title>" alt="<alt>">`,
645 },
646 {
647 "conditional valueless attr name",
648 `<input{{if .T}} checked{{end}} name=n>`,
649 `<input checked name=n>`,
650 },
651 {
652 "conditional dynamic valueless attr name 1",
653 `<input{{if .T}} {{"checked"}}{{end}} name=n>`,
654 `<input checked name=n>`,
655 },
656 {
657 "conditional dynamic valueless attr name 2",
658 `<input {{if .T}}{{"checked"}} {{end}}name=n>`,
659 `<input checked name=n>`,
660 },
661 {
662 "dynamic attribute name",
663 `<img on{{"load"}}="alert({{"loaded"}})">`,
664
665 `<img onload="alert("loaded")">`,
666 },
667 {
668 "bad dynamic attribute name 1",
669
670
671 `<input {{"onchange"}}="{{"doEvil()"}}">`,
672 `<input ZgotmplZ="doEvil()">`,
673 },
674 {
675 "bad dynamic attribute name 2",
676 `<div {{"sTyle"}}="{{"color: expression(alert(1337))"}}">`,
677 `<div ZgotmplZ="color: expression(alert(1337))">`,
678 },
679 {
680 "bad dynamic attribute name 3",
681
682 `<img {{"src"}}="{{"javascript:doEvil()"}}">`,
683 `<img ZgotmplZ="javascript:doEvil()">`,
684 },
685 {
686 "bad dynamic attribute name 4",
687
688
689 `<input checked {{""}}="Whose value am I?">`,
690 `<input checked ZgotmplZ="Whose value am I?">`,
691 },
692 {
693 "dynamic element name",
694 `<h{{3}}><table><t{{"head"}}>...</h{{3}}>`,
695 `<h3><table><thead>...</h3>`,
696 },
697 {
698 "bad dynamic element name",
699
700
701
702
703
704
705
706
707
708
709 `<{{"script"}}>{{"doEvil()"}}</{{"script"}}>`,
710 `<script>doEvil()</script>`,
711 },
712 {
713 "srcset bad URL in second position",
714 `<img srcset="{{"/not-an-image#,javascript:alert(1)"}}">`,
715
716 `<img srcset="/not-an-image#,#ZgotmplZ">`,
717 },
718 {
719 "srcset buffer growth",
720 `<img srcset={{",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"}}>`,
721 `<img srcset=,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,>`,
722 },
723 {
724 "unquoted empty attribute value (plaintext)",
725 "<p name={{.U}}>",
726 "<p name=ZgotmplZ>",
727 },
728 {
729 "unquoted empty attribute value (url)",
730 "<p href={{.U}}>",
731 "<p href=ZgotmplZ>",
732 },
733 {
734 "quoted empty attribute value",
735 "<p name=\"{{.U}}\">",
736 "<p name=\"\">",
737 },
738 {
739 "JS template lit special characters",
740 "<script>var a = `{{.I}}`</script>",
741 "<script>var a = `\\u0024\\u007b asd \\u0060\\u0060 \\u007d`</script>",
742 },
743 {
744 "JS template lit special characters, nested lit",
745 "<script>var a = `${ `{{.I}}` }`</script>",
746 "<script>var a = `${ `\\u0024\\u007b asd \\u0060\\u0060 \\u007d` }`</script>",
747 },
748 {
749 "JS template lit, nested JS",
750 "<script>var a = `${ var a = \"{{\"a \\\" d\"}}\" }`</script>",
751 "<script>var a = `${ var a = \"a \\u0022 d\" }`</script>",
752 },
753 {
754 "meta content attribute url",
755 `<meta http-equiv="refresh" content="asd; url={{"javascript:alert(1)"}}; asd; url={{"vbscript:alert(1)"}}; asd">`,
756 `<meta http-equiv="refresh" content="asd; url=#ZgotmplZ; asd; url=#ZgotmplZ; asd">`,
757 },
758 {
759 "meta content string",
760 `<meta http-equiv="refresh" content="{{"asd: 123"}}">`,
761 `<meta http-equiv="refresh" content="asd: 123">`,
762 },
763 {
764 "meta content url with whitespace before equals",
765 `<meta http-equiv="refresh" content="0;url ={{"javascript:alert(1)"}}">`,
766 `<meta http-equiv="refresh" content="0;url =#ZgotmplZ">`,
767 },
768 {
769 "meta content url with tab before equals",
770 "<meta http-equiv=\"refresh\" content=\"0;url\t={{\"javascript:alert(1)\"}}\">",
771 "<meta http-equiv=\"refresh\" content=\"0;url\t=#ZgotmplZ\">",
772 },
773 {
774 "meta content url with space after equals",
775 `<meta http-equiv="refresh" content="0;url= {{"javascript:alert(1)"}}">`,
776 `<meta http-equiv="refresh" content="0;url= #ZgotmplZ">`,
777 },
778 {
779 "meta content url with whitespace both sides of equals",
780 "<meta http-equiv=\"refresh\" content=\"0;url \t= {{\"javascript:alert(1)\"}}\">",
781 "<meta http-equiv=\"refresh\" content=\"0;url \t= #ZgotmplZ\">",
782 },
783 }
784
785 for _, test := range tests {
786 t.Run(test.name, func(t *testing.T) {
787 tmpl := New(test.name)
788 tmpl = Must(tmpl.Parse(test.input))
789
790 if tmpl.Tree != tmpl.text.Tree {
791 t.Fatalf("%s: tree not set properly", test.name)
792 }
793 b := new(strings.Builder)
794 if err := tmpl.Execute(b, data); err != nil {
795 t.Fatalf("%s: template execution failed: %s", test.name, err)
796 }
797 if w, g := test.output, b.String(); w != g {
798 t.Fatalf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g)
799 }
800 b.Reset()
801 if err := tmpl.Execute(b, pdata); err != nil {
802 t.Fatalf("%s: template execution failed for pointer: %s", test.name, err)
803 }
804 if w, g := test.output, b.String(); w != g {
805 t.Fatalf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g)
806 }
807 if tmpl.Tree != tmpl.text.Tree {
808 t.Fatalf("%s: tree mismatch", test.name)
809 }
810 })
811 }
812 }
813
814 func TestEscapeMap(t *testing.T) {
815 data := map[string]string{
816 "html": `<h1>Hi!</h1>`,
817 "urlquery": `http://www.foo.com/index.html?title=main`,
818 }
819 for _, test := range [...]struct {
820 desc, input, output string
821 }{
822
823 {
824 "field with predefined escaper name 1",
825 `{{.html | print}}`,
826 `<h1>Hi!</h1>`,
827 },
828
829 {
830 "field with predefined escaper name 2",
831 `{{.urlquery | print}}`,
832 `http://www.foo.com/index.html?title=main`,
833 },
834 } {
835 tmpl := Must(New("").Parse(test.input))
836 b := new(strings.Builder)
837 if err := tmpl.Execute(b, data); err != nil {
838 t.Errorf("%s: template execution failed: %s", test.desc, err)
839 continue
840 }
841 if w, g := test.output, b.String(); w != g {
842 t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.desc, w, g)
843 continue
844 }
845 }
846 }
847
848 func TestEscapeSet(t *testing.T) {
849 type dataItem struct {
850 Children []*dataItem
851 X string
852 }
853
854 data := dataItem{
855 Children: []*dataItem{
856 {X: "foo"},
857 {X: "<bar>"},
858 {
859 Children: []*dataItem{
860 {X: "baz"},
861 },
862 },
863 },
864 }
865
866 tests := []struct {
867 inputs map[string]string
868 want string
869 }{
870
871 {
872 map[string]string{
873 "main": ``,
874 },
875 ``,
876 },
877
878 {
879 map[string]string{
880 "main": `Hello, {{template "helper"}}!`,
881
882
883 "helper": `{{"<World>"}}`,
884 },
885 `Hello, <World>!`,
886 },
887
888 {
889 map[string]string{
890 "main": `<a onclick='a = {{template "helper"}};'>`,
891
892
893 "helper": `{{"<a>"}}<b`,
894 },
895 `<a onclick='a = "\u003ca\u003e"<b;'>`,
896 },
897
898 {
899 map[string]string{
900 "main": `{{range .Children}}{{template "main" .}}{{else}}{{.X}} {{end}}`,
901 },
902 `foo <bar> baz `,
903 },
904
905 {
906 map[string]string{
907 "main": `{{template "helper" .}}`,
908 "helper": `{{if .Children}}<ul>{{range .Children}}<li>{{template "main" .}}</li>{{end}}</ul>{{else}}{{.X}}{{end}}`,
909 },
910 `<ul><li>foo</li><li><bar></li><li><ul><li>baz</li></ul></li></ul>`,
911 },
912
913 {
914 map[string]string{
915 "main": `<blockquote>{{range .Children}}{{template "helper" .}}{{end}}</blockquote>`,
916 "helper": `{{if .Children}}{{template "main" .}}{{else}}{{.X}}<br>{{end}}`,
917 },
918 `<blockquote>foo<br><bar><br><blockquote>baz<br></blockquote></blockquote>`,
919 },
920
921 {
922 map[string]string{
923 "main": `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`,
924 "helper": `{{11}} of {{"<100>"}}`,
925 },
926 `<button onclick="title='11 of \u003c100\u003e'; ...">11 of <100></button>`,
927 },
928
929
930 {
931 map[string]string{
932 "main": `<script>var x={{template "helper"}}/{{"42"}};</script>`,
933 "helper": "{{126}}",
934 },
935 `<script>var x= 126 /"42";</script>`,
936 },
937
938 {
939 map[string]string{
940 "main": `<script>var x=[{{template "countdown" 4}}];</script>`,
941 "countdown": `{{.}}{{if .}},{{template "countdown" . | pred}}{{end}}`,
942 },
943 `<script>var x=[ 4 , 3 , 2 , 1 , 0 ];</script>`,
944 },
945
946
955 }
956
957
958
959 fns := FuncMap{"pred": func(a ...any) (any, error) {
960 if len(a) == 1 {
961 if i, _ := a[0].(int); i > 0 {
962 return i - 1, nil
963 }
964 }
965 return nil, fmt.Errorf("undefined pred(%v)", a)
966 }}
967
968 for _, test := range tests {
969 source := ""
970 for name, body := range test.inputs {
971 source += fmt.Sprintf("{{define %q}}%s{{end}} ", name, body)
972 }
973 tmpl, err := New("root").Funcs(fns).Parse(source)
974 if err != nil {
975 t.Errorf("error parsing %q: %v", source, err)
976 continue
977 }
978 var b strings.Builder
979
980 if err := tmpl.ExecuteTemplate(&b, "main", data); err != nil {
981 t.Errorf("%q executing %v", err.Error(), tmpl.Lookup("main"))
982 continue
983 }
984 if got := b.String(); test.want != got {
985 t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got)
986 }
987 }
988 }
989
990 func TestErrors(t *testing.T) {
991 tests := []struct {
992 input string
993 err string
994 }{
995
996 {
997 "{{if .Cond}}<a>{{else}}<b>{{end}}",
998 "",
999 },
1000 {
1001 "{{if .Cond}}<a>{{end}}",
1002 "",
1003 },
1004 {
1005 "{{if .Cond}}{{else}}<b>{{end}}",
1006 "",
1007 },
1008 {
1009 "{{with .Cond}}<div>{{end}}",
1010 "",
1011 },
1012 {
1013 "{{range .Items}}<a>{{end}}",
1014 "",
1015 },
1016 {
1017 "<a href='/foo?{{range .Items}}&{{.K}}={{.V}}{{end}}'>",
1018 "",
1019 },
1020 {
1021 "{{range .Items}}<a{{if .X}}{{end}}>{{end}}",
1022 "",
1023 },
1024 {
1025 "{{range .Items}}<a{{if .X}}{{end}}>{{continue}}{{end}}",
1026 "",
1027 },
1028 {
1029 "{{range .Items}}<a{{if .X}}{{end}}>{{break}}{{end}}",
1030 "",
1031 },
1032 {
1033 "{{range .Items}}<a{{if .X}}{{end}}>{{if .X}}{{break}}{{end}}{{end}}",
1034 "",
1035 },
1036 {
1037 "<script>var a = `${a+b}`</script>`",
1038 "",
1039 },
1040 {
1041 "<script>var tmpl = `asd`;</script>",
1042 ``,
1043 },
1044 {
1045 "<script>var tmpl = `${1}`;</script>",
1046 ``,
1047 },
1048 {
1049 "<script>var tmpl = `${return ``}`;</script>",
1050 ``,
1051 },
1052 {
1053 "<script>var tmpl = `${return {{.}} }`;</script>",
1054 ``,
1055 },
1056 {
1057 "<script>var tmpl = `${ let a = {1:1} {{.}} }`;</script>",
1058 ``,
1059 },
1060 {
1061 "<script>var tmpl = `asd ${return \"{\"}`;</script>",
1062 ``,
1063 },
1064 {
1065 `{{if eq "" ""}}<meta>{{end}}`,
1066 ``,
1067 },
1068 {
1069 `{{if eq "" ""}}<meta content="url={{"asd"}}">{{end}}`,
1070 ``,
1071 },
1072
1073
1074 {
1075 "{{if .Cond}}<a{{end}}",
1076 "z:1:5: {{if}} branches",
1077 },
1078 {
1079 "{{if .Cond}}\n{{else}}\n<a{{end}}",
1080 "z:1:5: {{if}} branches",
1081 },
1082 {
1083
1084 `{{if .Cond}}<a href="foo">{{else}}<a href="bar>{{end}}`,
1085 "z:1:5: {{if}} branches",
1086 },
1087 {
1088
1089 "<a {{if .Cond}}href='{{else}}title='{{end}}{{.X}}'>",
1090 "z:1:8: {{if}} branches",
1091 },
1092 {
1093 "\n{{with .X}}<a{{end}}",
1094 "z:2:7: {{with}} branches",
1095 },
1096 {
1097 "\n{{with .X}}<a>{{else}}<a{{end}}",
1098 "z:2:7: {{with}} branches",
1099 },
1100 {
1101 "{{range .Items}}<a{{end}}",
1102 `z:1: on range loop re-entry: "<" in attribute name: "<a"`,
1103 },
1104 {
1105 "\n{{range .Items}} x='<a{{end}}",
1106 "z:2:8: on range loop re-entry: {{range}} branches",
1107 },
1108 {
1109 "{{range .Items}}<a{{if .X}}{{break}}{{end}}>{{end}}",
1110 "z:1:29: at range loop break: {{range}} branches end in different contexts",
1111 },
1112 {
1113 "{{range .Items}}<a{{if .X}}{{continue}}{{end}}>{{end}}",
1114 "z:1:29: at range loop continue: {{range}} branches end in different contexts",
1115 },
1116 {
1117 "{{range .Items}}{{if .X}}{{break}}{{end}}<a{{if .Y}}{{continue}}{{end}}>{{if .Z}}{{continue}}{{end}}{{end}}",
1118 "z:1:54: at range loop continue: {{range}} branches end in different contexts",
1119 },
1120 {
1121 "<a b=1 c={{.H}}",
1122 "z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd",
1123 },
1124 {
1125 "<script>foo();",
1126 "z: ends in a non-text context: {stateJS",
1127 },
1128 {
1129 `<a href="{{if .F}}/foo?a={{else}}/bar/{{end}}{{.H}}">`,
1130 "z:1:47: {{.H}} appears in an ambiguous context within a URL",
1131 },
1132 {
1133 `<a onclick="alert('Hello \`,
1134 `unfinished escape sequence in JS string: "Hello \\"`,
1135 },
1136 {
1137 `<a onclick='alert("Hello\, World\`,
1138 `unfinished escape sequence in JS string: "Hello\\, World\\"`,
1139 },
1140 {
1141 `<a onclick='alert(/x+\`,
1142 `unfinished escape sequence in JS string: "x+\\"`,
1143 },
1144 {
1145 `<a onclick="/foo[\]/`,
1146 `unfinished JS regexp charset: "foo[\\]/"`,
1147 },
1148 {
1149
1150
1151
1152
1153
1154 `<script>{{if false}}var x = 1{{end}}/-{{"1.5"}}/i.test(x)</script>`,
1155 `'/' could start a division or regexp: "/-"`,
1156 },
1157 {
1158 `{{template "foo"}}`,
1159 "z:1:11: no such template \"foo\"",
1160 },
1161 {
1162 `<div{{template "y"}}>` +
1163
1164 `{{define "y"}} foo<b{{end}}`,
1165 `"<" in attribute name: " foo<b"`,
1166 },
1167 {
1168 `<script>reverseList = [{{template "t"}}]</script>` +
1169
1170 `{{define "t"}}{{if .Tail}}{{template "t" .Tail}}{{end}}{{.Head}}",{{end}}`,
1171 `: cannot compute output context for template t$htmltemplate_stateJS_elementScript`,
1172 },
1173 {
1174 `<input type=button value=onclick=>`,
1175 `html/template:z: "=" in unquoted attr: "onclick="`,
1176 },
1177 {
1178 `<input type=button value= onclick=>`,
1179 `html/template:z: "=" in unquoted attr: "onclick="`,
1180 },
1181 {
1182 `<input type=button value= 1+1=2>`,
1183 `html/template:z: "=" in unquoted attr: "1+1=2"`,
1184 },
1185 {
1186 "<a class=`foo>",
1187 "html/template:z: \"`\" in unquoted attr: \"`foo\"",
1188 },
1189 {
1190 `<a style=font:'Arial'>`,
1191 `html/template:z: "'" in unquoted attr: "font:'Arial'"`,
1192 },
1193 {
1194 `<a=foo>`,
1195 `: expected space, attr name, or end of tag, but got "=foo>"`,
1196 },
1197 {
1198 `Hello, {{. | urlquery | print}}!`,
1199
1200 `predefined escaper "urlquery" disallowed in template`,
1201 },
1202 {
1203 `Hello, {{. | html | print}}!`,
1204
1205 `predefined escaper "html" disallowed in template`,
1206 },
1207 {
1208 `Hello, {{html . | print}}!`,
1209
1210 `predefined escaper "html" disallowed in template`,
1211 },
1212 {
1213 `<div class={{. | html}}>Hello<div>`,
1214
1215
1216 `predefined escaper "html" disallowed in template`,
1217 },
1218 {
1219 `Hello, {{. | urlquery | html}}!`,
1220
1221 `predefined escaper "urlquery" disallowed in template`,
1222 },
1223 {
1224 "<script>var a = `{{if .X}}`{{end}}",
1225 `{{if}} branches end in different contexts`,
1226 },
1227 {
1228 "<script>var a = `{{if .X}}a{{else}}`{{end}}",
1229 `{{if}} branches end in different contexts`,
1230 },
1231 {
1232 "<script>var a = `{{if .X}}a{{else}}b{{end}}`</script>",
1233 ``,
1234 },
1235 }
1236 for _, test := range tests {
1237 buf := new(bytes.Buffer)
1238 tmpl, err := New("z").Parse(test.input)
1239 if err != nil {
1240 t.Errorf("input=%q: unexpected parse error %s\n", test.input, err)
1241 continue
1242 }
1243 err = tmpl.Execute(buf, nil)
1244 var got string
1245 if err != nil {
1246 got = err.Error()
1247 }
1248 if test.err == "" {
1249 if got != "" {
1250 t.Errorf("input=%q: unexpected error %q", test.input, got)
1251 }
1252 continue
1253 }
1254 if !strings.Contains(got, test.err) {
1255 t.Errorf("input=%q: error\n\t%q\ndoes not contain expected string\n\t%q", test.input, got, test.err)
1256 continue
1257 }
1258
1259 if err := tmpl.Execute(buf, nil); err == nil || err.Error() != got {
1260 t.Errorf("input=%q: unexpected error on second call %q", test.input, err)
1261 }
1262 }
1263 }
1264
1265 func TestEscapeText(t *testing.T) {
1266 tests := []struct {
1267 input string
1268 output context
1269 }{
1270 {
1271 ``,
1272 context{},
1273 },
1274 {
1275 `Hello, World!`,
1276 context{},
1277 },
1278 {
1279
1280 `I <3 Ponies!`,
1281 context{},
1282 },
1283 {
1284 `<a`,
1285 context{state: stateTag},
1286 },
1287 {
1288 `<a `,
1289 context{state: stateTag},
1290 },
1291 {
1292 `<a>`,
1293 context{state: stateText},
1294 },
1295 {
1296 `<a href`,
1297 context{state: stateAttrName, attr: attrURL},
1298 },
1299 {
1300 `<a on`,
1301 context{state: stateAttrName, attr: attrScript},
1302 },
1303 {
1304 `<a href `,
1305 context{state: stateAfterName, attr: attrURL},
1306 },
1307 {
1308 `<a style = `,
1309 context{state: stateBeforeValue, attr: attrStyle},
1310 },
1311 {
1312 `<a href=`,
1313 context{state: stateBeforeValue, attr: attrURL},
1314 },
1315 {
1316 `<a href=x`,
1317 context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery, attr: attrURL},
1318 },
1319 {
1320 `<a href=x `,
1321 context{state: stateTag},
1322 },
1323 {
1324 `<a href=>`,
1325 context{state: stateText},
1326 },
1327 {
1328 `<a href=x>`,
1329 context{state: stateText},
1330 },
1331 {
1332 `<a href ='`,
1333 context{state: stateURL, delim: delimSingleQuote, attr: attrURL},
1334 },
1335 {
1336 `<a href=''`,
1337 context{state: stateTag},
1338 },
1339 {
1340 `<a href= "`,
1341 context{state: stateURL, delim: delimDoubleQuote, attr: attrURL},
1342 },
1343 {
1344 `<a href=""`,
1345 context{state: stateTag},
1346 },
1347 {
1348 `<a title="`,
1349 context{state: stateAttr, delim: delimDoubleQuote},
1350 },
1351 {
1352 `<a HREF='http:`,
1353 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL},
1354 },
1355 {
1356 `<a Href='/`,
1357 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL},
1358 },
1359 {
1360 `<a href='"`,
1361 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL},
1362 },
1363 {
1364 `<a href="'`,
1365 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrURL},
1366 },
1367 {
1368 `<a href=''`,
1369 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL},
1370 },
1371 {
1372 `<a href=""`,
1373 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrURL},
1374 },
1375 {
1376 `<a href=""`,
1377 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrURL},
1378 },
1379 {
1380 `<a href="`,
1381 context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery, attr: attrURL},
1382 },
1383 {
1384 `<img alt="1">`,
1385 context{state: stateText},
1386 },
1387 {
1388 `<img alt="1>"`,
1389 context{state: stateTag},
1390 },
1391 {
1392 `<img alt="1>">`,
1393 context{state: stateText},
1394 },
1395 {
1396 `<input checked type="checkbox"`,
1397 context{state: stateTag},
1398 },
1399 {
1400 `<a onclick="`,
1401 context{state: stateJS, delim: delimDoubleQuote, attr: attrScript},
1402 },
1403 {
1404 `<a onclick="//foo`,
1405 context{state: stateJSLineCmt, delim: delimDoubleQuote, attr: attrScript},
1406 },
1407 {
1408 "<a onclick='//\n",
1409 context{state: stateJS, delim: delimSingleQuote, attr: attrScript},
1410 },
1411 {
1412 "<a onclick='//\r\n",
1413 context{state: stateJS, delim: delimSingleQuote, attr: attrScript},
1414 },
1415 {
1416 "<a onclick='//\u2028",
1417 context{state: stateJS, delim: delimSingleQuote, attr: attrScript},
1418 },
1419 {
1420 `<a onclick="/*`,
1421 context{state: stateJSBlockCmt, delim: delimDoubleQuote, attr: attrScript},
1422 },
1423 {
1424 `<a onclick="/*/`,
1425 context{state: stateJSBlockCmt, delim: delimDoubleQuote, attr: attrScript},
1426 },
1427 {
1428 `<a onclick="/**/`,
1429 context{state: stateJS, delim: delimDoubleQuote, attr: attrScript},
1430 },
1431 {
1432 `<a onkeypress=""`,
1433 context{state: stateJSDqStr, delim: delimDoubleQuote, attr: attrScript},
1434 },
1435 {
1436 `<a onclick='"foo"`,
1437 context{state: stateJS, delim: delimSingleQuote, jsCtx: jsCtxDivOp, attr: attrScript},
1438 },
1439 {
1440 `<a onclick='foo'`,
1441 context{state: stateJS, delim: delimSpaceOrTagEnd, jsCtx: jsCtxDivOp, attr: attrScript},
1442 },
1443 {
1444 `<a onclick='foo`,
1445 context{state: stateJSSqStr, delim: delimSpaceOrTagEnd, attr: attrScript},
1446 },
1447 {
1448 `<a onclick=""foo'`,
1449 context{state: stateJSDqStr, delim: delimDoubleQuote, attr: attrScript},
1450 },
1451 {
1452 `<a onclick="'foo"`,
1453 context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript},
1454 },
1455 {
1456 "<a onclick=\"`foo",
1457 context{state: stateJSTmplLit, delim: delimDoubleQuote, attr: attrScript},
1458 },
1459 {
1460 `<A ONCLICK="'`,
1461 context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript},
1462 },
1463 {
1464 `<a onclick="/`,
1465 context{state: stateJSRegexp, delim: delimDoubleQuote, attr: attrScript},
1466 },
1467 {
1468 `<a onclick="'foo'`,
1469 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript},
1470 },
1471 {
1472 `<a onclick="'foo\'`,
1473 context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript},
1474 },
1475 {
1476 `<a onclick="'foo\'`,
1477 context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript},
1478 },
1479 {
1480 `<a onclick="/foo/`,
1481 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript},
1482 },
1483 {
1484 `<script>/foo/ /=`,
1485 context{state: stateJS, element: elementScript},
1486 },
1487 {
1488 `<a onclick="1 /foo`,
1489 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript},
1490 },
1491 {
1492 `<a onclick="1 /*c*/ /foo`,
1493 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript},
1494 },
1495 {
1496 `<a onclick="/foo[/]`,
1497 context{state: stateJSRegexp, delim: delimDoubleQuote, attr: attrScript},
1498 },
1499 {
1500 `<a onclick="/foo\/`,
1501 context{state: stateJSRegexp, delim: delimDoubleQuote, attr: attrScript},
1502 },
1503 {
1504 `<a onclick="/foo/`,
1505 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript},
1506 },
1507 {
1508 `<input checked style="`,
1509 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle},
1510 },
1511 {
1512 `<a style="//`,
1513 context{state: stateCSSLineCmt, delim: delimDoubleQuote, attr: attrStyle},
1514 },
1515 {
1516 `<a style="//</script>`,
1517 context{state: stateCSSLineCmt, delim: delimDoubleQuote, attr: attrStyle},
1518 },
1519 {
1520 "<a style='//\n",
1521 context{state: stateCSS, delim: delimSingleQuote, attr: attrStyle},
1522 },
1523 {
1524 "<a style='//\r",
1525 context{state: stateCSS, delim: delimSingleQuote, attr: attrStyle},
1526 },
1527 {
1528 `<a style="/*`,
1529 context{state: stateCSSBlockCmt, delim: delimDoubleQuote, attr: attrStyle},
1530 },
1531 {
1532 `<a style="/*/`,
1533 context{state: stateCSSBlockCmt, delim: delimDoubleQuote, attr: attrStyle},
1534 },
1535 {
1536 `<a style="/**/`,
1537 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle},
1538 },
1539 {
1540 `<a style="background: '`,
1541 context{state: stateCSSSqStr, delim: delimDoubleQuote, attr: attrStyle},
1542 },
1543 {
1544 `<a style="background: "`,
1545 context{state: stateCSSDqStr, delim: delimDoubleQuote, attr: attrStyle},
1546 },
1547 {
1548 `<a style="background: '/foo?img=`,
1549 context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag, attr: attrStyle},
1550 },
1551 {
1552 `<a style="background: '/`,
1553 context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle},
1554 },
1555 {
1556 `<a style="background: url("/`,
1557 context{state: stateCSSDqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle},
1558 },
1559 {
1560 `<a style="background: url('/`,
1561 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle},
1562 },
1563 {
1564 `<a style="background: url('/)`,
1565 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle},
1566 },
1567 {
1568 `<a style="background: url('/ `,
1569 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle},
1570 },
1571 {
1572 `<a style="background: url(/`,
1573 context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle},
1574 },
1575 {
1576 `<a style="background: url( `,
1577 context{state: stateCSSURL, delim: delimDoubleQuote, attr: attrStyle},
1578 },
1579 {
1580 `<a style="background: url( /image?name=`,
1581 context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag, attr: attrStyle},
1582 },
1583 {
1584 `<a style="background: url(x)`,
1585 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle},
1586 },
1587 {
1588 `<a style="background: url('x'`,
1589 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle},
1590 },
1591 {
1592 `<a style="background: url( x `,
1593 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle},
1594 },
1595 {
1596 `<!-- foo`,
1597 context{state: stateHTMLCmt},
1598 },
1599 {
1600 `<!-->`,
1601 context{state: stateHTMLCmt},
1602 },
1603 {
1604 `<!--->`,
1605 context{state: stateHTMLCmt},
1606 },
1607 {
1608 `<!-- foo -->`,
1609 context{state: stateText},
1610 },
1611 {
1612 `<script`,
1613 context{state: stateTag, element: elementScript},
1614 },
1615 {
1616 `<script `,
1617 context{state: stateTag, element: elementScript},
1618 },
1619 {
1620 `<script src="foo.js" `,
1621 context{state: stateTag, element: elementScript},
1622 },
1623 {
1624 `<script src='foo.js' `,
1625 context{state: stateTag, element: elementScript},
1626 },
1627 {
1628 `<script type=text/javascript `,
1629 context{state: stateTag, element: elementScript},
1630 },
1631 {
1632 `<script>`,
1633 context{state: stateJS, jsCtx: jsCtxRegexp, element: elementScript},
1634 },
1635 {
1636 `<script>foo`,
1637 context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript},
1638 },
1639 {
1640 `<script>foo</script>`,
1641 context{state: stateText},
1642 },
1643 {
1644 `<script>foo</script><!--`,
1645 context{state: stateHTMLCmt},
1646 },
1647 {
1648 `<script>document.write("<p>foo</p>");`,
1649 context{state: stateJS, element: elementScript},
1650 },
1651 {
1652 `<script>document.write("<p>foo<\/script>");`,
1653 context{state: stateJS, element: elementScript},
1654 },
1655 {
1656
1657
1658 `<script>document.write("<script>alert(1)</script>");`,
1659 context{state: stateJS, element: elementScript},
1660 },
1661 {
1662 `<script>document.write("<script>`,
1663 context{state: stateJSDqStr, element: elementScript},
1664 },
1665 {
1666 `<script>document.write("<script>alert(1)</script>`,
1667 context{state: stateJSDqStr, element: elementScript},
1668 },
1669 {
1670 `<script>document.write("<script>alert(1)<!--`,
1671 context{state: stateJSDqStr, element: elementScript},
1672 },
1673 {
1674 `<script>document.write("<script>alert(1)</Script>");`,
1675 context{state: stateJS, element: elementScript},
1676 },
1677 {
1678 `<script>document.write("<!--");`,
1679 context{state: stateJS, element: elementScript},
1680 },
1681 {
1682 `<script>let a = /</script`,
1683 context{state: stateJSRegexp, element: elementScript},
1684 },
1685 {
1686 `<script>let a = /</script/`,
1687 context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp},
1688 },
1689 {
1690 `<script type="text/template">`,
1691 context{state: stateText},
1692 },
1693
1694 {
1695 `<script type="TEXT/JAVASCRIPT">`,
1696 context{state: stateJS, element: elementScript},
1697 },
1698
1699 {
1700 `<script TYPE="text/template">`,
1701 context{state: stateText},
1702 },
1703 {
1704 `<script type="notjs">`,
1705 context{state: stateText},
1706 },
1707 {
1708 `<Script>`,
1709 context{state: stateJS, element: elementScript},
1710 },
1711 {
1712 `<SCRIPT>foo`,
1713 context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript},
1714 },
1715 {
1716 `<textarea>value`,
1717 context{state: stateRCDATA, element: elementTextarea},
1718 },
1719 {
1720 `<textarea>value</TEXTAREA>`,
1721 context{state: stateText},
1722 },
1723 {
1724 `<textarea name=html><b`,
1725 context{state: stateRCDATA, element: elementTextarea},
1726 },
1727 {
1728 `<title>value`,
1729 context{state: stateRCDATA, element: elementTitle},
1730 },
1731 {
1732 `<style>value`,
1733 context{state: stateCSS, element: elementStyle},
1734 },
1735 {
1736 `<a xlink:href`,
1737 context{state: stateAttrName, attr: attrURL},
1738 },
1739 {
1740 `<a xmlns`,
1741 context{state: stateAttrName, attr: attrURL},
1742 },
1743 {
1744 `<a xmlns:foo`,
1745 context{state: stateAttrName, attr: attrURL},
1746 },
1747 {
1748 `<a xmlnsxyz`,
1749 context{state: stateAttrName},
1750 },
1751 {
1752 `<a data-url`,
1753 context{state: stateAttrName, attr: attrURL},
1754 },
1755 {
1756 `<a data-iconUri`,
1757 context{state: stateAttrName, attr: attrURL},
1758 },
1759 {
1760 `<a data-urlItem`,
1761 context{state: stateAttrName, attr: attrURL},
1762 },
1763 {
1764 `<a g:`,
1765 context{state: stateAttrName},
1766 },
1767 {
1768 `<a g:url`,
1769 context{state: stateAttrName, attr: attrURL},
1770 },
1771 {
1772 `<a g:iconUri`,
1773 context{state: stateAttrName, attr: attrURL},
1774 },
1775 {
1776 `<a g:urlItem`,
1777 context{state: stateAttrName, attr: attrURL},
1778 },
1779 {
1780 `<a g:value`,
1781 context{state: stateAttrName},
1782 },
1783 {
1784 `<a svg:style='`,
1785 context{state: stateCSS, delim: delimSingleQuote, attr: attrStyle},
1786 },
1787 {
1788 `<svg:font-face`,
1789 context{state: stateTag},
1790 },
1791 {
1792 `<svg:a svg:onclick="`,
1793 context{state: stateJS, delim: delimDoubleQuote, attr: attrScript},
1794 },
1795 {
1796 `<svg:a svg:onclick="x()">`,
1797 context{},
1798 },
1799 {
1800 "<script>var a = `",
1801 context{state: stateJSTmplLit, element: elementScript},
1802 },
1803 {
1804 "<script>var a = `${",
1805 context{state: stateJS, element: elementScript, jsBraceDepth: []int{0}},
1806 },
1807 {
1808 "<script>var a = `${}",
1809 context{state: stateJSTmplLit, element: elementScript},
1810 },
1811 {
1812 "<script>var a = `${`",
1813 context{state: stateJSTmplLit, element: elementScript, jsBraceDepth: []int{0}},
1814 },
1815 {
1816 "<script>var a = `${var a = \"",
1817 context{state: stateJSDqStr, element: elementScript, jsBraceDepth: []int{0}},
1818 },
1819 {
1820 "<script>var a = `${var a = \"`",
1821 context{state: stateJSDqStr, element: elementScript, jsBraceDepth: []int{0}},
1822 },
1823 {
1824 "<script>var a = `${var a = \"}",
1825 context{state: stateJSDqStr, element: elementScript, jsBraceDepth: []int{0}},
1826 },
1827 {
1828 "<script>var a = `${``",
1829 context{state: stateJS, element: elementScript, jsBraceDepth: []int{0}},
1830 },
1831 {
1832 "<script>var a = `${`}",
1833 context{state: stateJSTmplLit, element: elementScript, jsBraceDepth: []int{0}},
1834 },
1835 {
1836 "<script>`${ {} } asd`</script><script>`${ {} }",
1837 context{state: stateJSTmplLit, element: elementScript},
1838 },
1839 {
1840 "<script>var foo = `${ (_ => { return \"x\" })() + \"${",
1841 context{state: stateJSDqStr, element: elementScript, jsBraceDepth: []int{0}},
1842 },
1843 {
1844 "<script>var a = `${ {</script><script>var b = `${ x }",
1845 context{state: stateJSTmplLit, element: elementScript, jsCtx: jsCtxDivOp},
1846 },
1847 {
1848 "<script>var foo = `x` + \"${",
1849 context{state: stateJSDqStr, element: elementScript},
1850 },
1851 {
1852 "<script>function f() { var a = `${}`; }",
1853 context{state: stateJS, element: elementScript},
1854 },
1855 {
1856 "<script>{`${}`}",
1857 context{state: stateJS, element: elementScript},
1858 },
1859 {
1860 "<script>`${ function f() { return `${1}` }() }`",
1861 context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp},
1862 },
1863 {
1864 "<script>function f() {`${ function f() { `${1}` } }`}",
1865 context{state: stateJS, element: elementScript, jsCtx: jsCtxRegexp},
1866 },
1867 {
1868 "<script>`${ { `` }",
1869 context{state: stateJS, element: elementScript, jsBraceDepth: []int{0}},
1870 },
1871 {
1872 "<script>`${ { }`",
1873 context{state: stateJSTmplLit, element: elementScript, jsBraceDepth: []int{0}},
1874 },
1875 {
1876 "<script>var foo = `${ foo({ a: { c: `${",
1877 context{state: stateJS, element: elementScript, jsBraceDepth: []int{2, 0}},
1878 },
1879 {
1880 "<script>var foo = `${ foo({ a: { c: `${ {{.}} }` }, b: ",
1881 context{state: stateJS, element: elementScript, jsBraceDepth: []int{1}},
1882 },
1883 {
1884 "<script>`${ `}",
1885 context{state: stateJSTmplLit, element: elementScript, jsBraceDepth: []int{0}},
1886 },
1887 }
1888
1889 for _, test := range tests {
1890 b, e := []byte(test.input), makeEscaper(nil)
1891 c := e.escapeText(context{}, &parse.TextNode{NodeType: parse.NodeText, Text: b})
1892 if !test.output.eq(c) {
1893 t.Errorf("input %q: want context\n\t%v\ngot\n\t%v", test.input, test.output, c)
1894 continue
1895 }
1896 if test.input != string(b) {
1897 t.Errorf("input %q: text node was modified: want %q got %q", test.input, test.input, b)
1898 continue
1899 }
1900 }
1901 }
1902
1903 func TestEnsurePipelineContains(t *testing.T) {
1904 tests := []struct {
1905 input, output string
1906 ids []string
1907 }{
1908 {
1909 "{{.X}}",
1910 ".X",
1911 []string{},
1912 },
1913 {
1914 "{{.X | html}}",
1915 ".X | html",
1916 []string{},
1917 },
1918 {
1919 "{{.X}}",
1920 ".X | html",
1921 []string{"html"},
1922 },
1923 {
1924 "{{html .X}}",
1925 "_eval_args_ .X | html | urlquery",
1926 []string{"html", "urlquery"},
1927 },
1928 {
1929 "{{html .X .Y .Z}}",
1930 "_eval_args_ .X .Y .Z | html | urlquery",
1931 []string{"html", "urlquery"},
1932 },
1933 {
1934 "{{.X | print}}",
1935 ".X | print | urlquery",
1936 []string{"urlquery"},
1937 },
1938 {
1939 "{{.X | print | urlquery}}",
1940 ".X | print | urlquery",
1941 []string{"urlquery"},
1942 },
1943 {
1944 "{{.X | urlquery}}",
1945 ".X | html | urlquery",
1946 []string{"html", "urlquery"},
1947 },
1948 {
1949 "{{.X | print 2 | .f 3}}",
1950 ".X | print 2 | .f 3 | urlquery | html",
1951 []string{"urlquery", "html"},
1952 },
1953 {
1954
1955 "{{.X | println.x }}",
1956 ".X | println.x | urlquery | html",
1957 []string{"urlquery", "html"},
1958 },
1959 {
1960
1961 "{{.X | (print 12 | println).x }}",
1962 ".X | (print 12 | println).x | urlquery | html",
1963 []string{"urlquery", "html"},
1964 },
1965
1966
1967 {
1968 "{{.X | urlquery}}",
1969 ".X | _html_template_urlfilter | urlquery",
1970 []string{"_html_template_urlfilter", "_html_template_urlnormalizer"},
1971 },
1972 {
1973 "{{.X | urlquery}}",
1974 ".X | urlquery | _html_template_urlfilter | _html_template_cssescaper",
1975 []string{"_html_template_urlfilter", "_html_template_cssescaper"},
1976 },
1977 {
1978 "{{.X | urlquery}}",
1979 ".X | urlquery",
1980 []string{"_html_template_urlnormalizer"},
1981 },
1982 {
1983 "{{.X | urlquery}}",
1984 ".X | urlquery",
1985 []string{"_html_template_urlescaper"},
1986 },
1987 {
1988 "{{.X | html}}",
1989 ".X | html",
1990 []string{"_html_template_htmlescaper"},
1991 },
1992 {
1993 "{{.X | html}}",
1994 ".X | html",
1995 []string{"_html_template_rcdataescaper"},
1996 },
1997 }
1998 for i, test := range tests {
1999 tmpl := template.Must(template.New("test").Parse(test.input))
2000 action, ok := (tmpl.Tree.Root.Nodes[0].(*parse.ActionNode))
2001 if !ok {
2002 t.Errorf("First node is not an action: %s", test.input)
2003 continue
2004 }
2005 pipe := action.Pipe
2006 originalIDs := make([]string, len(test.ids))
2007 copy(originalIDs, test.ids)
2008 ensurePipelineContains(pipe, test.ids)
2009 got := pipe.String()
2010 if got != test.output {
2011 t.Errorf("#%d: %s, %v: want\n\t%s\ngot\n\t%s", i, test.input, originalIDs, test.output, got)
2012 }
2013 }
2014 }
2015
2016 func TestEscapeMalformedPipelines(t *testing.T) {
2017 tests := []string{
2018 "{{ 0 | $ }}",
2019 "{{ 0 | $ | urlquery }}",
2020 "{{ 0 | (nil) }}",
2021 "{{ 0 | (nil) | html }}",
2022 }
2023 for _, test := range tests {
2024 var b bytes.Buffer
2025 tmpl, err := New("test").Parse(test)
2026 if err != nil {
2027 t.Errorf("failed to parse set: %q", err)
2028 }
2029 err = tmpl.Execute(&b, nil)
2030 if err == nil {
2031 t.Errorf("Expected error for %q", test)
2032 }
2033 }
2034 }
2035
2036 func TestEscapeErrorsNotIgnorable(t *testing.T) {
2037 var b bytes.Buffer
2038 tmpl, _ := New("dangerous").Parse("<a")
2039 err := tmpl.Execute(&b, nil)
2040 if err == nil {
2041 t.Errorf("Expected error")
2042 } else if b.Len() != 0 {
2043 t.Errorf("Emitted output despite escaping failure")
2044 }
2045 }
2046
2047 func TestEscapeSetErrorsNotIgnorable(t *testing.T) {
2048 var b bytes.Buffer
2049 tmpl, err := New("root").Parse(`{{define "t"}}<a{{end}}`)
2050 if err != nil {
2051 t.Errorf("failed to parse set: %q", err)
2052 }
2053 err = tmpl.ExecuteTemplate(&b, "t", nil)
2054 if err == nil {
2055 t.Errorf("Expected error")
2056 } else if b.Len() != 0 {
2057 t.Errorf("Emitted output despite escaping failure")
2058 }
2059 }
2060
2061 func TestRedundantFuncs(t *testing.T) {
2062 inputs := []any{
2063 "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" +
2064 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
2065 ` !"#$%&'()*+,-./` +
2066 `0123456789:;<=>?` +
2067 `@ABCDEFGHIJKLMNO` +
2068 `PQRSTUVWXYZ[\]^_` +
2069 "`abcdefghijklmno" +
2070 "pqrstuvwxyz{|}~\x7f" +
2071 "\u00A0\u0100\u2028\u2029\ufeff\ufdec\ufffd\uffff\U0001D11E" +
2072 "&%22\\",
2073 CSS(`a[href =~ "//example.com"]#foo`),
2074 HTML(`Hello, <b>World</b> &tc!`),
2075 HTMLAttr(` dir="ltr"`),
2076 JS(`c && alert("Hello, World!");`),
2077 JSStr(`Hello, World & O'Reilly\x21`),
2078 URL(`greeting=H%69&addressee=(World)`),
2079 }
2080
2081 for n0, m := range redundantFuncs {
2082 f0 := funcMap[n0].(func(...any) string)
2083 for n1 := range m {
2084 f1 := funcMap[n1].(func(...any) string)
2085 for _, input := range inputs {
2086 want := f0(input)
2087 if got := f1(want); want != got {
2088 t.Errorf("%s %s with %T %q: want\n\t%q,\ngot\n\t%q", n0, n1, input, input, want, got)
2089 }
2090 }
2091 }
2092 }
2093 }
2094
2095 func TestIndirectPrint(t *testing.T) {
2096 a := 3
2097 ap := &a
2098 b := "hello"
2099 bp := &b
2100 bpp := &bp
2101 tmpl := Must(New("t").Parse(`{{.}}`))
2102 var buf strings.Builder
2103 err := tmpl.Execute(&buf, ap)
2104 if err != nil {
2105 t.Errorf("Unexpected error: %s", err)
2106 } else if buf.String() != "3" {
2107 t.Errorf(`Expected "3"; got %q`, buf.String())
2108 }
2109 buf.Reset()
2110 err = tmpl.Execute(&buf, bpp)
2111 if err != nil {
2112 t.Errorf("Unexpected error: %s", err)
2113 } else if buf.String() != "hello" {
2114 t.Errorf(`Expected "hello"; got %q`, buf.String())
2115 }
2116 }
2117
2118
2119 func TestEmptyTemplateHTML(t *testing.T) {
2120 page := Must(New("page").ParseFiles(os.DevNull))
2121 if err := page.ExecuteTemplate(os.Stdout, "page", "nothing"); err == nil {
2122 t.Fatal("expected error")
2123 }
2124 }
2125
2126 type Issue7379 int
2127
2128 func (Issue7379) SomeMethod(x int) string {
2129 return fmt.Sprintf("<%d>", x)
2130 }
2131
2132
2133
2134
2135
2136 func TestPipeToMethodIsEscaped(t *testing.T) {
2137 tmpl := Must(New("x").Parse("<html>{{0 | .SomeMethod}}</html>\n"))
2138 tryExec := func() string {
2139 defer func() {
2140 panicValue := recover()
2141 if panicValue != nil {
2142 t.Errorf("panicked: %v\n", panicValue)
2143 }
2144 }()
2145 var b strings.Builder
2146 tmpl.Execute(&b, Issue7379(0))
2147 return b.String()
2148 }
2149 for i := 0; i < 3; i++ {
2150 str := tryExec()
2151 const expect = "<html><0></html>\n"
2152 if str != expect {
2153 t.Errorf("expected %q got %q", expect, str)
2154 }
2155 }
2156 }
2157
2158
2159
2160
2161 func TestErrorOnUndefined(t *testing.T) {
2162 tmpl := New("undefined")
2163
2164 err := tmpl.Execute(nil, nil)
2165 if err == nil {
2166 t.Error("expected error")
2167 } else if !strings.Contains(err.Error(), "incomplete") {
2168 t.Errorf("expected error about incomplete template; got %s", err)
2169 }
2170 }
2171
2172
2173 func TestIdempotentExecute(t *testing.T) {
2174 tmpl := Must(New("").
2175 Parse(`{{define "main"}}<body>{{template "hello"}}</body>{{end}}`))
2176 Must(tmpl.
2177 Parse(`{{define "hello"}}Hello, {{"Ladies & Gentlemen!"}}{{end}}`))
2178 got := new(strings.Builder)
2179 var err error
2180
2181 want := "Hello, Ladies & Gentlemen!"
2182 for i := 0; i < 2; i++ {
2183 err = tmpl.ExecuteTemplate(got, "hello", nil)
2184 if err != nil {
2185 t.Errorf("unexpected error: %s", err)
2186 }
2187 if got.String() != want {
2188 t.Errorf("after executing template \"hello\", got:\n\t%q\nwant:\n\t%q\n", got.String(), want)
2189 }
2190 got.Reset()
2191 }
2192
2193
2194 err = tmpl.ExecuteTemplate(got, "main", nil)
2195 if err != nil {
2196 t.Errorf("unexpected error: %s", err)
2197 }
2198
2199
2200 want = "<body>Hello, Ladies & Gentlemen!</body>"
2201 if got.String() != want {
2202 t.Errorf("after executing template \"main\", got:\n\t%q\nwant:\n\t%q\n", got.String(), want)
2203 }
2204 }
2205
2206 func BenchmarkEscapedExecute(b *testing.B) {
2207 tmpl := Must(New("t").Parse(`<a onclick="alert('{{.}}')">{{.}}</a>`))
2208 var buf bytes.Buffer
2209 b.ResetTimer()
2210 for i := 0; i < b.N; i++ {
2211 tmpl.Execute(&buf, "foo & 'bar' & baz")
2212 buf.Reset()
2213 }
2214 }
2215
2216
2217 func TestOrphanedTemplate(t *testing.T) {
2218 t1 := Must(New("foo").Parse(`<a href="{{.}}">link1</a>`))
2219 t2 := Must(t1.New("foo").Parse(`bar`))
2220
2221 var b strings.Builder
2222 const wantError = `template: "foo" is an incomplete or empty template`
2223 if err := t1.Execute(&b, "javascript:alert(1)"); err == nil {
2224 t.Fatal("expected error executing t1")
2225 } else if gotError := err.Error(); gotError != wantError {
2226 t.Fatalf("got t1 execution error:\n\t%s\nwant:\n\t%s", gotError, wantError)
2227 }
2228 b.Reset()
2229 if err := t2.Execute(&b, nil); err != nil {
2230 t.Fatalf("error executing t2: %s", err)
2231 }
2232 const want = "bar"
2233 if got := b.String(); got != want {
2234 t.Fatalf("t2 rendered %q, want %q", got, want)
2235 }
2236 }
2237
2238
2239 func TestAliasedParseTreeDoesNotOverescape(t *testing.T) {
2240 const (
2241 tmplText = `{{.}}`
2242 data = `<baz>`
2243 want = `<baz>`
2244 )
2245
2246 tpl := Must(New("foo").Parse(tmplText))
2247 if _, err := tpl.AddParseTree("bar", tpl.Tree); err != nil {
2248 t.Fatalf("AddParseTree error: %v", err)
2249 }
2250 var b1, b2 strings.Builder
2251 if err := tpl.ExecuteTemplate(&b1, "foo", data); err != nil {
2252 t.Fatalf(`ExecuteTemplate failed for "foo": %v`, err)
2253 }
2254 if err := tpl.ExecuteTemplate(&b2, "bar", data); err != nil {
2255 t.Fatalf(`ExecuteTemplate failed for "foo": %v`, err)
2256 }
2257 got1, got2 := b1.String(), b2.String()
2258 if got1 != want {
2259 t.Fatalf(`Template "foo" rendered %q, want %q`, got1, want)
2260 }
2261 if got1 != got2 {
2262 t.Fatalf(`Template "foo" and "bar" rendered %q and %q respectively, expected equal values`, got1, got2)
2263 }
2264 }
2265
2266 func TestMetaContentEscapeGODEBUG(t *testing.T) {
2267 testenv.SetGODEBUG(t, "htmlmetacontenturlescape=0")
2268 tmpl := Must(New("").Parse(`<meta http-equiv="refresh" content="asd; url={{"javascript:alert(1)"}}; asd; url={{"vbscript:alert(1)"}}; asd">`))
2269 var b strings.Builder
2270 if err := tmpl.Execute(&b, nil); err != nil {
2271 t.Fatalf("unexpected error: %s", err)
2272 }
2273 want := `<meta http-equiv="refresh" content="asd; url=javascript:alert(1); asd; url=vbscript:alert(1); asd">`
2274 if got := b.String(); got != want {
2275 t.Fatalf("got %q, want %q", got, want)
2276 }
2277 }
2278
2279 func TestCVE202656858(t *testing.T) {
2280 tests := []struct {
2281 name string
2282 tmpl string
2283 input string
2284 want string
2285 }{
2286 {
2287 name: "regexp after open brace in if block",
2288 tmpl: `<script>if(true){/{{.}}/g.test("x")}</script>`,
2289 input: "a.b",
2290 want: `<script>if(true){/a\.b/g.test("x")}</script>`,
2291 },
2292 {
2293 name: "regexp after close brace",
2294 tmpl: `<script>if(true){x=1}/{{.}}/g.test("x")</script>`,
2295 input: "a.b",
2296 want: `<script>if(true){x=1}/a\.b/g.test("x")</script>`,
2297 },
2298 {
2299 name: "regexp pathological attacker input",
2300 tmpl: `<script>if(true){/{{.}}/g.test("x")}</script>`,
2301 input: `./;alert(1);var q=/.`,
2302 want: `<script>if(true){/\.\/;alert\(1\);var q=\/\./g.test("x")}</script>`,
2303 },
2304 {
2305 name: "regexp after open brace in template literal",
2306 tmpl: "<script>`${ (function(){/{{.}}/g.test(x)}) }`</script>",
2307 input: "a.b",
2308 want: "<script>`${ (function(){/a\\.b/g.test(x)}) }`</script>",
2309 },
2310 {
2311 name: "regexp after close brace in template literal",
2312 tmpl: "<script>`${ (function(){}/{{.}}/g.test(x)) }`</script>",
2313 input: "a.b",
2314 want: "<script>`${ (function(){}/a\\.b/g.test(x)) }`</script>",
2315 },
2316 }
2317 for _, tt := range tests {
2318 t.Run(tt.name, func(t *testing.T) {
2319 tmpl := Must(New("test").Parse(tt.tmpl))
2320 var buf bytes.Buffer
2321 if err := tmpl.Execute(&buf, tt.input); err != nil {
2322 t.Fatalf("Execute: %v", err)
2323 }
2324 if got := buf.String(); got != tt.want {
2325 t.Errorf("got: %s\nwant: %s", got, tt.want)
2326 }
2327 })
2328 }
2329 }
2330
View as plain text