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