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