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.
18 type badMarshaler struct{}
20 func (x *badMarshaler) MarshalJSON() ([]byte, error) {
21 // Keys in valid JSON must be double quoted as must all strings.
22 return []byte("{ foo: 'not quite valid JSON' }"), nil
25 type goodMarshaler struct{}
27 func (x *goodMarshaler) MarshalJSON() ([]byte, error) {
28 return []byte(`{ "<foo>": "O'Reilly" }`), nil
31 func TestEscape(t *testing.T) {
46 A: []string{"<a>", "<b>"},
52 W: HTML(`¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`),
63 "{{if .T}}Hello{{end}}, {{.C}}!",
64 "Hello, <Cincinatti>!",
68 "{{if .F}}{{.H}}{{else}}{{.G}}{{end}}!",
73 "Hello, {{.C | html}}!",
74 "Hello, <Cincinatti>!",
78 "Hello, {{html .C}}!",
79 "Hello, <Cincinatti>!",
83 "{{with .C}}{{$msg := .}}Hello, {{$msg}}!{{end}}",
84 "Hello, <Cincinatti>!",
88 "{{if $x := .H}}{{$x}}{{end}}",
93 "{{with .H}}{{.}}{{end}}",
98 "{{with .E}}{{.}}{{else}}{{.H}}{{end}}",
103 "{{range .A}}{{.}}{{end}}",
104 "<a><b>",
108 "{{range .E}}{{.}}{{else}}{{.H}}{{end}}",
118 `<a href="/search?q={{"'a<b'"}}">`,
119 `<a href="/search?q=%27a%3cb%27">`,
124 "<a b=1 c=<Hello>>",
128 `<a href='{{"/foo/bar?a=b&c=d"}}'>`,
129 `<a href='/foo/bar?a=b&c=d'>`,
133 `<a href='{{"http://example.com/foo/bar?a=b&c=d"}}'>`,
134 `<a href='http://example.com/foo/bar?a=b&c=d'>`,
137 "protocolRelativeURLStart",
138 `<a href='{{"//example.com:8000/foo/bar?a=b&c=d"}}'>`,
139 `<a href='//example.com:8000/foo/bar?a=b&c=d'>`,
142 "pathRelativeURLStart",
143 `<a href="{{"/javascript:80/foo/bar"}}">`,
144 `<a href="/javascript:80/foo/bar">`,
148 `<a href='{{"javascript:alert(%22pwned%22)"}}'>`,
149 `<a href='#ZgotmplZ'>`,
152 "dangerousURLStart2",
153 `<a href=' {{"javascript:alert(%22pwned%22)"}}'>`,
154 `<a href=' #ZgotmplZ'>`,
158 `<a href={{"mailto:Muhammed \"The Greatest\" Ali <m.ali@example.com>"}}>`,
159 `<a href=mailto:Muhammed%20%22The%20Greatest%22%20Ali%20%3cm.ali@example.com%3e>`,
163 `<a href='http://{{"javascript:80"}}/foo'>`,
164 `<a href='http://javascript:80/foo'>`,
168 `<a href='/search?q={{.H}}'>`,
169 `<a href='/search?q=%3cHello%3e'>`,
173 `<a href='/faq#{{.H}}'>`,
174 `<a href='/faq#%3cHello%3e'>`,
178 `<a href="{{if .F}}/foo?a=b{{else}}/bar{{end}}">`,
182 "urlBranchConflictMoot",
183 `<a href="{{if .T}}/foo?a={{else}}/bar#{{end}}{{.C}}">`,
184 `<a href="/foo?a=%3cCincinatti%3e">`,
188 "<button onclick='alert({{.H}})'>",
189 `<button onclick='alert("\u003cHello\u003e")'>`,
193 "<button onclick='alert({{.N}})'>",
194 `<button onclick='alert( 42 )'>`,
198 "<button onclick='alert({{.T}})'>",
199 `<button onclick='alert( true )'>`,
203 "<button onclick='alert(typeof{{.Z}})'>",
204 `<button onclick='alert(typeof null )'>`,
208 "<button onclick='alert({{.A}})'>",
209 `<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`,
213 "<script>alert({{.A}})</script>",
214 `<script>alert(["\u003ca\u003e","\u003cb\u003e"])</script>`,
217 "jsObjValueNotOverEscaped",
218 "<button onclick='alert({{.A | html}})'>",
219 `<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`,
223 "<button onclick='alert("{{.H}}")'>",
224 `<button onclick='alert("\x3cHello\x3e")'>`,
228 `<button onclick='alert(1/{{.B}}in numbers)'>`,
229 `<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)'>`,
233 `<button onclick='alert({{.M}})'>`,
234 `<button onclick='alert({"\u003cfoo\u003e":"O'Reilly"})'>`,
237 "jsStrNotUnderEscaped",
238 "<button onclick='alert({{.C | urlquery}})'>",
239 // URL escaped, then quoted for JS.
240 `<button onclick='alert("%3CCincinatti%3E")'>`,
244 `<button onclick='alert(/{{"foo+bar"}}/.test(""))'>`,
245 `<button onclick='alert(/foo\x2bbar/.test(""))'>`,
249 `<script>alert(/{{""}}/.test(""));</script>`,
250 `<script>alert(/(?:)/.test(""));</script>`,
254 `<script>{{if true}}var x = 1{{end}}</script>`,
255 // The {if} ends in an ambiguous jsCtx but there is
256 // no slash following so we shouldn't care.
257 `<script>var x = 1</script>`,
260 "styleBidiKeywordPassed",
261 `<p style="dir: {{"ltr"}}">`,
262 `<p style="dir: ltr">`,
265 "styleBidiPropNamePassed",
266 `<p style="border-{{"left"}}: 0; border-{{"right"}}: 1in">`,
267 `<p style="border-left: 0; border-right: 1in">`,
270 "styleExpressionBlocked",
271 `<p style="width: {{"expression(alert(1337))"}}">`,
272 `<p style="width: ZgotmplZ">`,
275 "styleTagSelectorPassed",
276 `<style>{{"p"}} { color: pink }</style>`,
277 `<style>p { color: pink }</style>`,
281 `<style>p{{"#my-ID"}} { font: Arial }</style>`,
282 `<style>p#my-ID { font: Arial }</style>`,
286 `<style>p{{".my_class"}} { font: Arial }</style>`,
287 `<style>p.my_class { font: Arial }</style>`,
290 "styleQuantityPassed",
291 `<a style="left: {{"2em"}}; top: {{0}}">`,
292 `<a style="left: 2em; top: 0">`,
296 `<table style=width:{{"100%"}}>`,
297 `<table style=width:100%>`,
301 `<p style="color: {{"#8ff"}}; background: {{"#000"}}">`,
302 `<p style="color: #8ff; background: #000">`,
305 "styleObfuscatedExpressionBlocked",
306 `<p style="width: {{" e\\78preS\x00Sio/**/n(alert(1337))"}}">`,
307 `<p style="width: ZgotmplZ">`,
310 "styleMozBindingBlocked",
311 `<p style="{{"-moz-binding(alert(1337))"}}: ...">`,
312 `<p style="ZgotmplZ: ...">`,
315 "styleObfuscatedMozBindingBlocked",
316 `<p style="{{" -mo\\7a-B\x00I/**/nding(alert(1337))"}}: ...">`,
317 `<p style="ZgotmplZ: ...">`,
320 "styleFontNameString",
321 `<p style='font-family: "{{"Times New Roman"}}"'>`,
322 `<p style='font-family: "Times New Roman"'>`,
325 "styleFontNameString",
326 `<p style='font-family: "{{"Times New Roman"}}", "{{"sans-serif"}}"'>`,
327 `<p style='font-family: "Times New Roman", "sans-serif"'>`,
330 "styleFontNameUnquoted",
331 `<p style='font-family: {{"Times New Roman"}}'>`,
332 `<p style='font-family: Times New Roman'>`,
335 "styleURLQueryEncoded",
336 `<p style="background: url(/img?name={{"O'Reilly Animal(1)<2>.png"}})">`,
337 `<p style="background: url(/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png)">`,
340 "styleQuotedURLQueryEncoded",
341 `<p style="background: url('/img?name={{"O'Reilly Animal(1)<2>.png"}}')">`,
342 `<p style="background: url('/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png')">`,
345 "styleStrQueryEncoded",
346 `<p style="background: '/img?name={{"O'Reilly Animal(1)<2>.png"}}'">`,
347 `<p style="background: '/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png'">`,
350 "styleURLBadProtocolBlocked",
351 `<a style="background: url('{{"javascript:alert(1337)"}}')">`,
352 `<a style="background: url('#ZgotmplZ')">`,
355 "styleStrBadProtocolBlocked",
356 `<a style="background: '{{"vbscript:alert(1337)"}}'">`,
357 `<a style="background: '#ZgotmplZ'">`,
360 "styleStrEncodedProtocolEncoded",
361 `<a style="background: '{{"javascript\\3a alert(1337)"}}'">`,
362 // The CSS string 'javascript\\3a alert(1337)' does not contains a colon.
363 `<a style="background: 'javascript\\3a alert\28 1337\29 '">`,
366 "styleURLGoodProtocolPassed",
367 `<a style="background: url('{{"http://oreilly.com/O'Reilly Animals(1)<2>;{}.html"}}')">`,
368 `<a style="background: url('http://oreilly.com/O%27Reilly%20Animals%281%29%3c2%3e;%7b%7d.html')">`,
371 "styleStrGoodProtocolPassed",
372 `<a style="background: '{{"http://oreilly.com/O'Reilly Animals(1)<2>;{}.html"}}'">`,
373 `<a style="background: 'http\3a\2f\2foreilly.com\2fO\27Reilly Animals\28 1\29\3c 2\3e\3b\7b\7d.html'">`,
376 "styleURLEncodedForHTMLInAttr",
377 `<a style="background: url('{{"/search?img=foo&size=icon"}}')">`,
378 `<a style="background: url('/search?img=foo&size=icon')">`,
381 "styleURLNotEncodedForHTMLInCdata",
382 `<style>body { background: url('{{"/search?img=foo&size=icon"}}') }</style>`,
383 `<style>body { background: url('/search?img=foo&size=icon') }</style>`,
387 `<p style="background: URL(#{{.H}})">`,
388 `<p style="background: URL(#%3cHello%3e)">`,
391 "stylePropertyPairPassed",
392 `<a style='{{"color: red"}}'>`,
393 `<a style='color: red'>`,
396 "styleStrSpecialsEncoded",
397 `<a style="font-family: '{{"/**/'\";:// \\"}}', "{{"/**/'\";:// \\"}}"">`,
398 `<a style="font-family: '\2f**\2f\27\22\3b\3a\2f\2f \\', "\2f**\2f\27\22\3b\3a\2f\2f \\"">`,
401 "styleURLSpecialsEncoded",
402 `<a style="border-image: url({{"/**/'\";:// \\"}}), url("{{"/**/'\";:// \\"}}"), url('{{"/**/'\";:// \\"}}'), 'http://www.example.com/?q={{"/**/'\";:// \\"}}''">`,
403 `<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''">`,
407 "<b>Hello, <!-- name of world -->{{.C}}</b>",
408 "<b>Hello, <Cincinatti></b>",
411 "HTML comment not first < in text node.",
416 "HTML normalization 1",
421 "HTML normalization 2",
426 "HTML normalization 3",
427 "a<<!-- --><!-- -->b",
431 "HTML doctype not normalized",
432 "<!DOCTYPE html>Hello, World!",
433 "<!DOCTYPE html>Hello, World!",
436 "HTML doctype not case-insensitive",
437 "<!doCtYPE htMl>Hello, World!",
438 "<!doCtYPE htMl>Hello, World!",
441 "No doctype injection",
446 "Split HTML comment",
447 "<b>Hello, <!-- name of {{if .T}}city -->{{.C}}{{else}}world -->{{.W}}{{end}}</b>",
448 "<b>Hello, <Cincinatti></b>",
452 "<script>for (;;) { if (c()) break// foo not a label\n" +
453 "foo({{.T}});}</script>",
454 "<script>for (;;) { if (c()) break\n" +
455 "foo( true );}</script>",
458 "JS multiline block comment",
459 "<script>for (;;) { if (c()) break/* foo not a label\n" +
460 " */foo({{.T}});}</script>",
461 // Newline separates break from call. If newline
462 // removed, then break will consume label leaving
464 "<script>for (;;) { if (c()) break\n" +
465 "foo( true );}</script>",
468 "JS single-line block comment",
469 "<script>for (;;) {\n" +
470 "if (c()) break/* foo a label */foo;" +
471 "x({{.T}});}</script>",
472 // Newline separates break from call. If newline
473 // removed, then break will consume label leaving
475 "<script>for (;;) {\n" +
476 "if (c()) break foo;" +
477 "x( true );}</script>",
480 "JS block comment flush with mathematical division",
481 "<script>var a/*b*//c\nd</script>",
482 "<script>var a /c\nd</script>",
486 "<script>var a/*b*///c\nd</script>",
487 "<script>var a \nd</script>",
491 "<style>p// paragraph\n" +
492 `{border: 1px/* color */{{"#00f"}}}</style>`,
494 "{border: 1px #00f}</style>",
497 "JS attr block comment",
498 `<a onclick="f(""); /* alert({{.H}}) */">`,
499 // Attribute comment tests should pass if the comments
500 // are successfully elided.
501 `<a onclick="f(""); /* alert() */">`,
504 "JS attr line comment",
505 `<a onclick="// alert({{.G}})">`,
506 `<a onclick="// alert()">`,
509 "CSS attr block comment",
510 `<a style="/* color: {{.H}} */">`,
511 `<a style="/* color: */">`,
514 "CSS attr line comment",
515 `<a style="// color: {{.G}}">`,
516 `<a style="// color: ">`,
519 "HTML substitution commented out",
520 "<p><!-- {{.H}} --></p>",
524 "Comment ends flush with start",
525 "<!--{{.}}--><script>/*{{.}}*///{{.}}\n</script><style>/*{{.}}*///{{.}}\n</style><a onclick='/*{{.}}*///{{.}}' style='/*{{.}}*///{{.}}'>",
526 "<script> \n</script><style> \n</style><a onclick='/**///' style='/**///'>",
529 "typed HTML in text",
531 `¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`,
534 "typed HTML in attribute",
535 `<div title="{{.W}}">`,
536 `<div title="¡Hello, O'World!">`,
539 "typed HTML in script",
540 `<button onclick="alert({{.W}})">`,
541 `<button onclick="alert("&iexcl;\u003cb class=\"foo\"\u003eHello\u003c/b\u003e, \u003ctextarea\u003eO'World\u003c/textarea\u003e!")">`,
544 "typed HTML in RCDATA",
545 `<textarea>{{.W}}</textarea>`,
546 `<textarea>¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!</textarea>`,
550 "<textarea>{{range .A}}{{.}}{{end}}</textarea>",
551 "<textarea><a><b></textarea>",
554 "auditable exemption from escaping",
555 "{{range .A}}{{. | noescape}}{{end}}",
560 `{{"10$"}}<{{"script src,evil.org/pwnd.js"}}...`,
561 `10$<script src,evil.org/pwnd.js...`,
564 "No comment injection",
569 "No RCDATA end tag injection",
570 `<textarea><{{"/textarea "}}...</textarea>`,
571 `<textarea></textarea ...</textarea>`,
575 `<img class="{{"iconClass"}}"` +
576 `{{if .T}} id="{{"<iconId>"}}"{{end}}` +
577 // Double quotes inside if/else.
579 `{{if .T}}"?{{"<iconPath>"}}"` +
580 `{{else}}"images/cleardot.gif"{{end}}` +
581 // Missing space before title, but it is not a
582 // part of the src attribute.
583 `{{if .T}}title="{{"<title>"}}"{{end}}` +
584 // Quotes outside if/else.
586 `{{if .T}}{{"<alt>"}}` +
587 `{{else}}{{if .F}}{{"<title>"}}{{end}}` +
590 `<img class="iconClass" id="<iconId>" src="?%3ciconPath%3e"title="<title>" alt="<alt>">`,
593 "conditional valueless attr name",
594 `<input{{if .T}} checked{{end}} name=n>`,
595 `<input checked name=n>`,
598 "conditional dynamic valueless attr name 1",
599 `<input{{if .T}} {{"checked"}}{{end}} name=n>`,
600 `<input checked name=n>`,
603 "conditional dynamic valueless attr name 2",
604 `<input {{if .T}}{{"checked"}} {{end}}name=n>`,
605 `<input checked name=n>`,
608 "dynamic attribute name",
609 `<img on{{"load"}}="alert({{"loaded"}})">`,
610 // Treated as JS since quotes are inserted.
611 `<img onload="alert("loaded")">`,
614 "bad dynamic attribute name 1",
615 // Allow checked, selected, disabled, but not JS or
617 `<input {{"onchange"}}="{{"doEvil()"}}">`,
618 `<input ZgotmplZ="doEvil()">`,
621 "bad dynamic attribute name 2",
622 `<div {{"sTyle"}}="{{"color: expression(alert(1337))"}}">`,
623 `<div ZgotmplZ="color: expression(alert(1337))">`,
626 "bad dynamic attribute name 3",
627 // Allow title or alt, but not a URL.
628 `<img {{"src"}}="{{"javascript:doEvil()"}}">`,
629 `<img ZgotmplZ="javascript:doEvil()">`,
632 "bad dynamic attribute name 4",
633 // Structure preservation requires values to associate
634 // with a consistent attribute.
635 `<input checked {{""}}="Whose value am I?">`,
636 `<input checked ZgotmplZ="Whose value am I?">`,
639 "dynamic element name",
640 `<h{{3}}><table><t{{"head"}}>...</h{{3}}>`,
641 `<h3><table><thead>...</h3>`,
644 "bad dynamic element name",
645 // Dynamic element names are typically used to switch
646 // between (thead, tfoot, tbody), (ul, ol), (th, td),
647 // and other replaceable sets.
648 // We do not currently easily support (ul, ol).
649 // If we do change to support that, this test should
650 // catch failures to filter out special tag names which
651 // would violate the structure preservation property --
652 // if any special tag name could be substituted, then
653 // the content could be raw text/RCDATA for some inputs
654 // and regular HTML content for others.
655 `<{{"script"}}>{{"doEvil()"}}</{{"script"}}>`,
656 `<script>doEvil()</script>`,
660 for _, test := range tests {
661 tmpl := New(test.name)
662 // TODO: Move noescape into template/func.go
664 "noescape": func(a ...interface{}) string {
665 return fmt.Sprint(a...)
668 tmpl = Must(tmpl.Parse(test.input))
669 b := new(bytes.Buffer)
670 if err := tmpl.Execute(b, data); err != nil {
671 t.Errorf("%s: template execution failed: %s", test.name, err)
674 if w, g := test.output, b.String(); w != g {
675 t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g)
679 if err := tmpl.Execute(b, pdata); err != nil {
680 t.Errorf("%s: template execution failed for pointer: %s", test.name, err)
683 if w, g := test.output, b.String(); w != g {
684 t.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g)
690 func TestEscapeSet(t *testing.T) {
691 type dataItem struct {
697 Children: []*dataItem{
701 Children: []*dataItem{
709 inputs map[string]string
719 // A template called in the start context.
722 "main": `Hello, {{template "helper"}}!`,
723 // Not a valid top level HTML template.
724 // "<b" is not a full tag.
725 "helper": `{{"<World>"}}`,
727 `Hello, <World>!`,
729 // A template called in a context other than the start.
732 "main": `<a onclick='a = {{template "helper"}};'>`,
733 // Not a valid top level HTML template.
734 // "<b" is not a full tag.
735 "helper": `{{"<a>"}}<b`,
737 `<a onclick='a = "\u003ca\u003e"<b;'>`,
739 // A recursive template that ends in its start context.
742 "main": `{{range .Children}}{{template "main" .}}{{else}}{{.X}} {{end}}`,
744 `foo <bar> baz `,
746 // A recursive helper template that ends in its start context.
749 "main": `{{template "helper" .}}`,
750 "helper": `{{if .Children}}<ul>{{range .Children}}<li>{{template "main" .}}</li>{{end}}</ul>{{else}}{{.X}}{{end}}`,
752 `<ul><li>foo</li><li><bar></li><li><ul><li>baz</li></ul></li></ul>`,
754 // Co-recursive templates that end in its start context.
757 "main": `<blockquote>{{range .Children}}{{template "helper" .}}{{end}}</blockquote>`,
758 "helper": `{{if .Children}}{{template "main" .}}{{else}}{{.X}}<br>{{end}}`,
760 `<blockquote>foo<br><bar><br><blockquote>baz<br></blockquote></blockquote>`,
762 // A template that is called in two different contexts.
765 "main": `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`,
766 "helper": `{{11}} of {{"<100>"}}`,
768 `<button onclick="title='11 of \x3c100\x3e'; ...">11 of <100></button>`,
770 // A non-recursive template that ends in a different context.
771 // helper starts in jsCtxRegexp and ends in jsCtxDivOp.
774 "main": `<script>var x={{template "helper"}}/{{"42"}};</script>`,
777 `<script>var x= 126 /"42";</script>`,
779 // A recursive template that ends in a similar context.
782 "main": `<script>var x=[{{template "countdown" 4}}];</script>`,
783 "countdown": `{{.}}{{if .}},{{template "countdown" . | pred}}{{end}}`,
785 `<script>var x=[ 4 , 3 , 2 , 1 , 0 ];</script>`,
787 // A recursive template that ends in a different context.
791 "main": `<a href="/foo{{template "helper" .}}">`,
792 "helper": `{{if .Children}}{{range .Children}}{{template "helper" .}}{{end}}{{else}}?x={{.X}}{{end}}`,
794 `<a href="/foo?x=foo?x=%3cbar%3e?x=baz">`,
799 // pred is a template function that returns the predecessor of a
800 // natural number for testing recursive templates.
801 fns := FuncMap{"pred": func(a ...interface{}) (interface{}, error) {
803 if i, _ := a[0].(int); i > 0 {
807 return nil, fmt.Errorf("undefined pred(%v)", a)
810 for _, test := range tests {
812 for name, body := range test.inputs {
813 source += fmt.Sprintf("{{define %q}}%s{{end}} ", name, body)
815 tmpl, err := New("root").Funcs(fns).Parse(source)
817 t.Errorf("error parsing %q: %v", source, err)
822 if err := tmpl.ExecuteTemplate(&b, "main", data); err != nil {
823 t.Errorf("%q executing %v", err.Error(), tmpl.Lookup("main"))
826 if got := b.String(); test.want != got {
827 t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got)
833 func TestErrors(t *testing.T) {
840 "{{if .Cond}}<a>{{else}}<b>{{end}}",
844 "{{if .Cond}}<a>{{end}}",
848 "{{if .Cond}}{{else}}<b>{{end}}",
852 "{{with .Cond}}<div>{{end}}",
856 "{{range .Items}}<a>{{end}}",
860 "<a href='/foo?{{range .Items}}&{{.K}}={{.V}}{{end}}'>",
865 "{{if .Cond}}<a{{end}}",
866 "z:1: {{if}} branches",
869 "{{if .Cond}}\n{{else}}\n<a{{end}}",
870 "z:1: {{if}} branches",
873 // Missing quote in the else branch.
874 `{{if .Cond}}<a href="foo">{{else}}<a href="bar>{{end}}`,
875 "z:1: {{if}} branches",
878 // Different kind of attribute: href implies a URL.
879 "<a {{if .Cond}}href='{{else}}title='{{end}}{{.X}}'>",
880 "z:1: {{if}} branches",
883 "\n{{with .X}}<a{{end}}",
884 "z:2: {{with}} branches",
887 "\n{{with .X}}<a>{{else}}<a{{end}}",
888 "z:2: {{with}} branches",
891 "{{range .Items}}<a{{end}}",
892 `z:1: on range loop re-entry: "<" in attribute name: "<a"`,
895 "\n{{range .Items}} x='<a{{end}}",
896 "z:2: on range loop re-entry: {{range}} branches",
900 "z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd",
904 "z: ends in a non-text context: {stateJS",
907 `<a href="{{if .F}}/foo?a={{else}}/bar/{{end}}{{.H}}">`,
908 "z:1: {{.H}} appears in an ambiguous URL context",
911 `<a onclick="alert('Hello \`,
912 `unfinished escape sequence in JS string: "Hello \\"`,
915 `<a onclick='alert("Hello\, World\`,
916 `unfinished escape sequence in JS string: "Hello\\, World\\"`,
919 `<a onclick='alert(/x+\`,
920 `unfinished escape sequence in JS string: "x+\\"`,
923 `<a onclick="/foo[\]/`,
924 `unfinished JS regexp charset: "foo[\\]/"`,
927 // It is ambiguous whether 1.5 should be 1\.5 or 1.5.
928 // Either `var x = 1/- 1.5 /i.test(x)`
929 // where `i.test(x)` is a method call of reference i,
930 // or `/-1\.5/i.test(x)` which is a method call on a
931 // case insensitive regular expression.
932 `<script>{{if false}}var x = 1{{end}}/-{{"1.5"}}/i.test(x)</script>`,
933 `'/' could start a division or regexp: "/-"`,
936 `{{template "foo"}}`,
937 "z:1: no such template \"foo\"",
940 `<div{{template "y"}}>` +
941 // Illegal starting in stateTag but not in stateText.
942 `{{define "y"}} foo<b{{end}}`,
943 `"<" in attribute name: " foo<b"`,
946 `<script>reverseList = [{{template "t"}}]</script>` +
947 // Missing " after recursive call.
948 `{{define "t"}}{{if .Tail}}{{template "t" .Tail}}{{end}}{{.Head}}",{{end}}`,
949 `: cannot compute output context for template t$htmltemplate_stateJS_elementScript`,
952 `<input type=button value=onclick=>`,
953 `html/template:z: "=" in unquoted attr: "onclick="`,
956 `<input type=button value= onclick=>`,
957 `html/template:z: "=" in unquoted attr: "onclick="`,
960 `<input type=button value= 1+1=2>`,
961 `html/template:z: "=" in unquoted attr: "1+1=2"`,
965 "html/template:z: \"`\" in unquoted attr: \"`foo\"",
968 `<a style=font:'Arial'>`,
969 `html/template:z: "'" in unquoted attr: "font:'Arial'"`,
973 `: expected space, attr name, or end of tag, but got "=foo>"`,
977 for _, test := range tests {
978 buf := new(bytes.Buffer)
979 tmpl, err := New("z").Parse(test.input)
981 t.Errorf("input=%q: unexpected parse error %s\n", test.input, err)
984 err = tmpl.Execute(buf, nil)
991 t.Errorf("input=%q: unexpected error %q", test.input, got)
995 if strings.Index(got, test.err) == -1 {
996 t.Errorf("input=%q: error\n\t%q\ndoes not contain expected string\n\t%q", test.input, got, test.err)
1002 func TestEscapeText(t *testing.T) {
1016 // An orphaned "<" is OK.
1022 context{state: stateTag},
1026 context{state: stateTag},
1030 context{state: stateText},
1034 context{state: stateAttrName, attr: attrURL},
1038 context{state: stateAttrName, attr: attrScript},
1042 context{state: stateAfterName, attr: attrURL},
1046 context{state: stateBeforeValue, attr: attrStyle},
1050 context{state: stateBeforeValue, attr: attrURL},
1054 context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery},
1058 context{state: stateTag},
1062 context{state: stateText},
1066 context{state: stateText},
1070 context{state: stateURL, delim: delimSingleQuote},
1074 context{state: stateTag},
1078 context{state: stateURL, delim: delimDoubleQuote},
1082 context{state: stateTag},
1086 context{state: stateAttr, delim: delimDoubleQuote},
1090 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery},
1094 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery},
1098 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery},
1102 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery},
1106 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery},
1110 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery},
1114 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery},
1118 context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery},
1122 context{state: stateText},
1126 context{state: stateTag},
1130 context{state: stateText},
1133 `<input checked type="checkbox"`,
1134 context{state: stateTag},
1138 context{state: stateJS, delim: delimDoubleQuote},
1141 `<a onclick="//foo`,
1142 context{state: stateJSLineCmt, delim: delimDoubleQuote},
1146 context{state: stateJS, delim: delimSingleQuote},
1149 "<a onclick='//\r\n",
1150 context{state: stateJS, delim: delimSingleQuote},
1153 "<a onclick='//\u2028",
1154 context{state: stateJS, delim: delimSingleQuote},
1158 context{state: stateJSBlockCmt, delim: delimDoubleQuote},
1162 context{state: stateJSBlockCmt, delim: delimDoubleQuote},
1166 context{state: stateJS, delim: delimDoubleQuote},
1169 `<a onkeypress=""`,
1170 context{state: stateJSDqStr, delim: delimDoubleQuote},
1173 `<a onclick='"foo"`,
1174 context{state: stateJS, delim: delimSingleQuote, jsCtx: jsCtxDivOp},
1177 `<a onclick='foo'`,
1178 context{state: stateJS, delim: delimSpaceOrTagEnd, jsCtx: jsCtxDivOp},
1181 `<a onclick='foo`,
1182 context{state: stateJSSqStr, delim: delimSpaceOrTagEnd},
1185 `<a onclick=""foo'`,
1186 context{state: stateJSDqStr, delim: delimDoubleQuote},
1189 `<a onclick="'foo"`,
1190 context{state: stateJSSqStr, delim: delimDoubleQuote},
1194 context{state: stateJSSqStr, delim: delimDoubleQuote},
1198 context{state: stateJSRegexp, delim: delimDoubleQuote},
1201 `<a onclick="'foo'`,
1202 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp},
1205 `<a onclick="'foo\'`,
1206 context{state: stateJSSqStr, delim: delimDoubleQuote},
1209 `<a onclick="'foo\'`,
1210 context{state: stateJSSqStr, delim: delimDoubleQuote},
1213 `<a onclick="/foo/`,
1214 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp},
1218 context{state: stateJS, element: elementScript},
1221 `<a onclick="1 /foo`,
1222 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp},
1225 `<a onclick="1 /*c*/ /foo`,
1226 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp},
1229 `<a onclick="/foo[/]`,
1230 context{state: stateJSRegexp, delim: delimDoubleQuote},
1233 `<a onclick="/foo\/`,
1234 context{state: stateJSRegexp, delim: delimDoubleQuote},
1237 `<a onclick="/foo/`,
1238 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp},
1241 `<input checked style="`,
1242 context{state: stateCSS, delim: delimDoubleQuote},
1246 context{state: stateCSSLineCmt, delim: delimDoubleQuote},
1249 `<a style="//</script>`,
1250 context{state: stateCSSLineCmt, delim: delimDoubleQuote},
1254 context{state: stateCSS, delim: delimSingleQuote},
1258 context{state: stateCSS, delim: delimSingleQuote},
1262 context{state: stateCSSBlockCmt, delim: delimDoubleQuote},
1266 context{state: stateCSSBlockCmt, delim: delimDoubleQuote},
1270 context{state: stateCSS, delim: delimDoubleQuote},
1273 `<a style="background: '`,
1274 context{state: stateCSSSqStr, delim: delimDoubleQuote},
1277 `<a style="background: "`,
1278 context{state: stateCSSDqStr, delim: delimDoubleQuote},
1281 `<a style="background: '/foo?img=`,
1282 context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag},
1285 `<a style="background: '/`,
1286 context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartPreQuery},
1289 `<a style="background: url("/`,
1290 context{state: stateCSSDqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery},
1293 `<a style="background: url('/`,
1294 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery},
1297 `<a style="background: url('/)`,
1298 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery},
1301 `<a style="background: url('/ `,
1302 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery},
1305 `<a style="background: url(/`,
1306 context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery},
1309 `<a style="background: url( `,
1310 context{state: stateCSSURL, delim: delimDoubleQuote},
1313 `<a style="background: url( /image?name=`,
1314 context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag},
1317 `<a style="background: url(x)`,
1318 context{state: stateCSS, delim: delimDoubleQuote},
1321 `<a style="background: url('x'`,
1322 context{state: stateCSS, delim: delimDoubleQuote},
1325 `<a style="background: url( x `,
1326 context{state: stateCSS, delim: delimDoubleQuote},
1330 context{state: stateHTMLCmt},
1334 context{state: stateHTMLCmt},
1338 context{state: stateHTMLCmt},
1342 context{state: stateText},
1346 context{state: stateTag, element: elementScript},
1350 context{state: stateTag, element: elementScript},
1353 `<script src="foo.js" `,
1354 context{state: stateTag, element: elementScript},
1357 `<script src='foo.js' `,
1358 context{state: stateTag, element: elementScript},
1361 `<script type=text/javascript `,
1362 context{state: stateTag, element: elementScript},
1366 context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript},
1369 `<script>foo</script>`,
1370 context{state: stateText},
1373 `<script>foo</script><!--`,
1374 context{state: stateHTMLCmt},
1377 `<script>document.write("<p>foo</p>");`,
1378 context{state: stateJS, element: elementScript},
1381 `<script>document.write("<p>foo<\/script>");`,
1382 context{state: stateJS, element: elementScript},
1385 `<script>document.write("<script>alert(1)</script>");`,
1386 context{state: stateText},
1390 context{state: stateJS, element: elementScript},
1394 context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript},
1398 context{state: stateRCDATA, element: elementTextarea},
1401 `<textarea>value</TEXTAREA>`,
1402 context{state: stateText},
1405 `<textarea name=html><b`,
1406 context{state: stateRCDATA, element: elementTextarea},
1410 context{state: stateRCDATA, element: elementTitle},
1414 context{state: stateCSS, element: elementStyle},
1418 context{state: stateAttrName, attr: attrURL},
1422 context{state: stateAttrName, attr: attrURL},
1426 context{state: stateAttrName, attr: attrURL},
1430 context{state: stateAttrName},
1434 context{state: stateAttrName, attr: attrURL},
1438 context{state: stateAttrName, attr: attrURL},
1442 context{state: stateAttrName, attr: attrURL},
1446 context{state: stateAttrName},
1450 context{state: stateAttrName, attr: attrURL},
1454 context{state: stateAttrName, attr: attrURL},
1458 context{state: stateAttrName, attr: attrURL},
1462 context{state: stateAttrName},
1466 context{state: stateCSS, delim: delimSingleQuote},
1470 context{state: stateTag},
1473 `<svg:a svg:onclick="`,
1474 context{state: stateJS, delim: delimDoubleQuote},
1478 for _, test := range tests {
1479 b, e := []byte(test.input), newEscaper(nil)
1480 c := e.escapeText(context{}, &parse.TextNode{NodeType: parse.NodeText, Text: b})
1481 if !test.output.eq(c) {
1482 t.Errorf("input %q: want context\n\t%v\ngot\n\t%v", test.input, test.output, c)
1485 if test.input != string(b) {
1486 t.Errorf("input %q: text node was modified: want %q got %q", test.input, test.input, b)
1492 func TestEnsurePipelineContains(t *testing.T) {
1494 input, output string
1514 ".X | html | urlquery",
1515 []string{"urlquery"},
1518 "{{.X | html | urlquery}}",
1519 ".X | html | urlquery",
1520 []string{"urlquery"},
1523 "{{.X | html | urlquery}}",
1524 ".X | html | urlquery",
1525 []string{"html", "urlquery"},
1528 "{{.X | html | urlquery}}",
1529 ".X | html | urlquery",
1533 "{{.X | urlquery}}",
1534 ".X | html | urlquery",
1535 []string{"html", "urlquery"},
1538 "{{.X | html | print}}",
1539 ".X | urlquery | html | print",
1540 []string{"urlquery", "html"},
1543 for i, test := range tests {
1544 tmpl := template.Must(template.New("test").Parse(test.input))
1545 action, ok := (tmpl.Tree.Root.Nodes[0].(*parse.ActionNode))
1547 t.Errorf("#%d: First node is not an action: %s", i, test.input)
1551 ensurePipelineContains(pipe, test.ids)
1552 got := pipe.String()
1553 if got != test.output {
1554 t.Errorf("#%d: %s, %v: want\n\t%s\ngot\n\t%s", i, test.input, test.ids, test.output, got)
1559 func TestEscapeErrorsNotIgnorable(t *testing.T) {
1561 tmpl, _ := New("dangerous").Parse("<a")
1562 err := tmpl.Execute(&b, nil)
1564 t.Errorf("Expected error")
1565 } else if b.Len() != 0 {
1566 t.Errorf("Emitted output despite escaping failure")
1570 func TestEscapeSetErrorsNotIgnorable(t *testing.T) {
1572 tmpl, err := New("root").Parse(`{{define "t"}}<a{{end}}`)
1574 t.Errorf("failed to parse set: %q", err)
1576 err = tmpl.ExecuteTemplate(&b, "t", nil)
1578 t.Errorf("Expected error")
1579 } else if b.Len() != 0 {
1580 t.Errorf("Emitted output despite escaping failure")
1584 func TestRedundantFuncs(t *testing.T) {
1585 inputs := []interface{}{
1586 "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" +
1587 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
1588 ` !"#$%&'()*+,-./` +
1589 `0123456789:;<=>?` +
1590 `@ABCDEFGHIJKLMNO` +
1591 `PQRSTUVWXYZ[\]^_` +
1592 "`abcdefghijklmno" +
1593 "pqrstuvwxyz{|}~\x7f" +
1594 "\u00A0\u0100\u2028\u2029\ufeff\ufdec\ufffd\uffff\U0001D11E" +
1596 CSS(`a[href =~ "//example.com"]#foo`),
1597 HTML(`Hello, <b>World</b> &tc!`),
1598 HTMLAttr(` dir="ltr"`),
1599 JS(`c && alert("Hello, World!");`),
1600 JSStr(`Hello, World & O'Reilly\x21`),
1601 URL(`greeting=H%69&addressee=(World)`),
1604 for n0, m := range redundantFuncs {
1605 f0 := funcMap[n0].(func(...interface{}) string)
1607 f1 := funcMap[n1].(func(...interface{}) string)
1608 for _, input := range inputs {
1610 if got := f1(want); want != got {
1611 t.Errorf("%s %s with %T %q: want\n\t%q,\ngot\n\t%q", n0, n1, input, input, want, got)
1618 func TestIndirectPrint(t *testing.T) {
1624 tmpl := Must(New("t").Parse(`{{.}}`))
1625 var buf bytes.Buffer
1626 err := tmpl.Execute(&buf, ap)
1628 t.Errorf("Unexpected error: %s", err)
1629 } else if buf.String() != "3" {
1630 t.Errorf(`Expected "3"; got %q`, buf.String())
1633 err = tmpl.Execute(&buf, bpp)
1635 t.Errorf("Unexpected error: %s", err)
1636 } else if buf.String() != "hello" {
1637 t.Errorf(`Expected "hello"; got %q`, buf.String())
1641 // This is a test for issue 3272.
1642 func TestEmptyTemplate(t *testing.T) {
1643 page := Must(New("page").ParseFiles(os.DevNull))
1644 if err := page.ExecuteTemplate(os.Stdout, "page", "nothing"); err == nil {
1645 t.Fatal("expected error")
1649 func BenchmarkEscapedExecute(b *testing.B) {
1650 tmpl := Must(New("t").Parse(`<a onclick="alert('{{.}}')">{{.}}</a>`))
1651 var buf bytes.Buffer
1653 for i := 0; i < b.N; i++ {
1654 tmpl.Execute(&buf, "foo & 'bar' & baz")