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