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