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