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("\u0026iexcl;\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>",
555 `{{"10$"}}<{{"script src,evil.org/pwnd.js"}}...`,
556 `10$<script src,evil.org/pwnd.js...`,
559 "No comment injection",
564 "No RCDATA end tag injection",
565 `<textarea><{{"/textarea "}}...</textarea>`,
566 `<textarea></textarea ...</textarea>`,
570 `<img class="{{"iconClass"}}"` +
571 `{{if .T}} id="{{"<iconId>"}}"{{end}}` +
572 // Double quotes inside if/else.
574 `{{if .T}}"?{{"<iconPath>"}}"` +
575 `{{else}}"images/cleardot.gif"{{end}}` +
576 // Missing space before title, but it is not a
577 // part of the src attribute.
578 `{{if .T}}title="{{"<title>"}}"{{end}}` +
579 // Quotes outside if/else.
581 `{{if .T}}{{"<alt>"}}` +
582 `{{else}}{{if .F}}{{"<title>"}}{{end}}` +
585 `<img class="iconClass" id="<iconId>" src="?%3ciconPath%3e"title="<title>" alt="<alt>">`,
588 "conditional valueless attr name",
589 `<input{{if .T}} checked{{end}} name=n>`,
590 `<input checked name=n>`,
593 "conditional dynamic valueless attr name 1",
594 `<input{{if .T}} {{"checked"}}{{end}} name=n>`,
595 `<input checked name=n>`,
598 "conditional dynamic valueless attr name 2",
599 `<input {{if .T}}{{"checked"}} {{end}}name=n>`,
600 `<input checked name=n>`,
603 "dynamic attribute name",
604 `<img on{{"load"}}="alert({{"loaded"}})">`,
605 // Treated as JS since quotes are inserted.
606 `<img onload="alert("loaded")">`,
609 "bad dynamic attribute name 1",
610 // Allow checked, selected, disabled, but not JS or
612 `<input {{"onchange"}}="{{"doEvil()"}}">`,
613 `<input ZgotmplZ="doEvil()">`,
616 "bad dynamic attribute name 2",
617 `<div {{"sTyle"}}="{{"color: expression(alert(1337))"}}">`,
618 `<div ZgotmplZ="color: expression(alert(1337))">`,
621 "bad dynamic attribute name 3",
622 // Allow title or alt, but not a URL.
623 `<img {{"src"}}="{{"javascript:doEvil()"}}">`,
624 `<img ZgotmplZ="javascript:doEvil()">`,
627 "bad dynamic attribute name 4",
628 // Structure preservation requires values to associate
629 // with a consistent attribute.
630 `<input checked {{""}}="Whose value am I?">`,
631 `<input checked ZgotmplZ="Whose value am I?">`,
634 "dynamic element name",
635 `<h{{3}}><table><t{{"head"}}>...</h{{3}}>`,
636 `<h3><table><thead>...</h3>`,
639 "bad dynamic element name",
640 // Dynamic element names are typically used to switch
641 // between (thead, tfoot, tbody), (ul, ol), (th, td),
642 // and other replaceable sets.
643 // We do not currently easily support (ul, ol).
644 // If we do change to support that, this test should
645 // catch failures to filter out special tag names which
646 // would violate the structure preservation property --
647 // if any special tag name could be substituted, then
648 // the content could be raw text/RCDATA for some inputs
649 // and regular HTML content for others.
650 `<{{"script"}}>{{"doEvil()"}}</{{"script"}}>`,
651 `<script>doEvil()</script>`,
655 for _
, test
:= range tests
{
656 tmpl
:= New(test
.name
)
657 tmpl
= Must(tmpl
.Parse(test
.input
))
658 // Check for bug 6459: Tree field was not set in Parse.
659 if tmpl
.Tree
!= tmpl
.text
.Tree
{
660 t
.Errorf("%s: tree not set properly", test
.name
)
663 b
:= new(bytes
.Buffer
)
664 if err
:= tmpl
.Execute(b
, data
); err
!= nil {
665 t
.Errorf("%s: template execution failed: %s", test
.name
, err
)
668 if w
, g
:= test
.output
, b
.String(); w
!= g
{
669 t
.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test
.name
, w
, g
)
673 if err
:= tmpl
.Execute(b
, pdata
); err
!= nil {
674 t
.Errorf("%s: template execution failed for pointer: %s", test
.name
, err
)
677 if w
, g
:= test
.output
, b
.String(); w
!= g
{
678 t
.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test
.name
, w
, g
)
681 if tmpl
.Tree
!= tmpl
.text
.Tree
{
682 t
.Errorf("%s: tree mismatch", test
.name
)
688 func TestEscapeSet(t
*testing
.T
) {
689 type dataItem
struct {
695 Children
: []*dataItem
{
699 Children
: []*dataItem
{
707 inputs
map[string]string
717 // A template called in the start context.
720 "main": `Hello, {{template "helper"}}!`,
721 // Not a valid top level HTML template.
722 // "<b" is not a full tag.
723 "helper": `{{"<World>"}}`,
725 `Hello, <World>!`,
727 // A template called in a context other than the start.
730 "main": `<a onclick='a = {{template "helper"}};'>`,
731 // Not a valid top level HTML template.
732 // "<b" is not a full tag.
733 "helper": `{{"<a>"}}<b`,
735 `<a onclick='a = "\u003ca\u003e"<b;'>`,
737 // A recursive template that ends in its start context.
740 "main": `{{range .Children}}{{template "main" .}}{{else}}{{.X}} {{end}}`,
742 `foo <bar> baz `,
744 // A recursive helper template that ends in its start context.
747 "main": `{{template "helper" .}}`,
748 "helper": `{{if .Children}}<ul>{{range .Children}}<li>{{template "main" .}}</li>{{end}}</ul>{{else}}{{.X}}{{end}}`,
750 `<ul><li>foo</li><li><bar></li><li><ul><li>baz</li></ul></li></ul>`,
752 // Co-recursive templates that end in its start context.
755 "main": `<blockquote>{{range .Children}}{{template "helper" .}}{{end}}</blockquote>`,
756 "helper": `{{if .Children}}{{template "main" .}}{{else}}{{.X}}<br>{{end}}`,
758 `<blockquote>foo<br><bar><br><blockquote>baz<br></blockquote></blockquote>`,
760 // A template that is called in two different contexts.
763 "main": `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`,
764 "helper": `{{11}} of {{"<100>"}}`,
766 `<button onclick="title='11 of \x3c100\x3e'; ...">11 of <100></button>`,
768 // A non-recursive template that ends in a different context.
769 // helper starts in jsCtxRegexp and ends in jsCtxDivOp.
772 "main": `<script>var x={{template "helper"}}/{{"42"}};</script>`,
775 `<script>var x= 126 /"42";</script>`,
777 // A recursive template that ends in a similar context.
780 "main": `<script>var x=[{{template "countdown" 4}}];</script>`,
781 "countdown": `{{.}}{{if .}},{{template "countdown" . | pred}}{{end}}`,
783 `<script>var x=[ 4 , 3 , 2 , 1 , 0 ];</script>`,
785 // A recursive template that ends in a different context.
789 "main": `<a href="/foo{{template "helper" .}}">`,
790 "helper": `{{if .Children}}{{range .Children}}{{template "helper" .}}{{end}}{{else}}?x={{.X}}{{end}}`,
792 `<a href="/foo?x=foo?x=%3cbar%3e?x=baz">`,
797 // pred is a template function that returns the predecessor of a
798 // natural number for testing recursive templates.
799 fns
:= FuncMap
{"pred": func(a
...interface{}) (interface{}, error
) {
801 if i
, _
:= a
[0].(int); i
> 0 {
805 return nil, fmt
.Errorf("undefined pred(%v)", a
)
808 for _
, test
:= range tests
{
810 for name
, body
:= range test
.inputs
{
811 source
+= fmt
.Sprintf("{{define %q}}%s{{end}} ", name
, body
)
813 tmpl
, err
:= New("root").Funcs(fns
).Parse(source
)
815 t
.Errorf("error parsing %q: %v", source
, err
)
820 if err
:= tmpl
.ExecuteTemplate(&b
, "main", data
); err
!= nil {
821 t
.Errorf("%q executing %v", err
.Error(), tmpl
.Lookup("main"))
824 if got
:= b
.String(); test
.want
!= got
{
825 t
.Errorf("want\n\t%q\ngot\n\t%q", test
.want
, got
)
831 func TestErrors(t
*testing
.T
) {
838 "{{if .Cond}}<a>{{else}}<b>{{end}}",
842 "{{if .Cond}}<a>{{end}}",
846 "{{if .Cond}}{{else}}<b>{{end}}",
850 "{{with .Cond}}<div>{{end}}",
854 "{{range .Items}}<a>{{end}}",
858 "<a href='/foo?{{range .Items}}&{{.K}}={{.V}}{{end}}'>",
863 "{{if .Cond}}<a{{end}}",
864 "z:1: {{if}} branches",
867 "{{if .Cond}}\n{{else}}\n<a{{end}}",
868 "z:1: {{if}} branches",
871 // Missing quote in the else branch.
872 `{{if .Cond}}<a href="foo">{{else}}<a href="bar>{{end}}`,
873 "z:1: {{if}} branches",
876 // Different kind of attribute: href implies a URL.
877 "<a {{if .Cond}}href='{{else}}title='{{end}}{{.X}}'>",
878 "z:1: {{if}} branches",
881 "\n{{with .X}}<a{{end}}",
882 "z:2: {{with}} branches",
885 "\n{{with .X}}<a>{{else}}<a{{end}}",
886 "z:2: {{with}} branches",
889 "{{range .Items}}<a{{end}}",
890 `z:1: on range loop re-entry: "<" in attribute name: "<a"`,
893 "\n{{range .Items}} x='<a{{end}}",
894 "z:2: on range loop re-entry: {{range}} branches",
898 "z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd",
902 "z: ends in a non-text context: {stateJS",
905 `<a href="{{if .F}}/foo?a={{else}}/bar/{{end}}{{.H}}">`,
906 "z:1: {{.H}} appears in an ambiguous URL context",
909 `<a onclick="alert('Hello \`,
910 `unfinished escape sequence in JS string: "Hello \\"`,
913 `<a onclick='alert("Hello\, World\`,
914 `unfinished escape sequence in JS string: "Hello\\, World\\"`,
917 `<a onclick='alert(/x+\`,
918 `unfinished escape sequence in JS string: "x+\\"`,
921 `<a onclick="/foo[\]/`,
922 `unfinished JS regexp charset: "foo[\\]/"`,
925 // It is ambiguous whether 1.5 should be 1\.5 or 1.5.
926 // Either `var x = 1/- 1.5 /i.test(x)`
927 // where `i.test(x)` is a method call of reference i,
928 // or `/-1\.5/i.test(x)` which is a method call on a
929 // case insensitive regular expression.
930 `<script>{{if false}}var x = 1{{end}}/-{{"1.5"}}/i.test(x)</script>`,
931 `'/' could start a division or regexp: "/-"`,
934 `{{template "foo"}}`,
935 "z:1: no such template \"foo\"",
938 `<div{{template "y"}}>` +
939 // Illegal starting in stateTag but not in stateText.
940 `{{define "y"}} foo<b{{end}}`,
941 `"<" in attribute name: " foo<b"`,
944 `<script>reverseList = [{{template "t"}}]</script>` +
945 // Missing " after recursive call.
946 `{{define "t"}}{{if .Tail}}{{template "t" .Tail}}{{end}}{{.Head}}",{{end}}`,
947 `: cannot compute output context for template t$htmltemplate_stateJS_elementScript`,
950 `<input type=button value=onclick=>`,
951 `html/template:z: "=" in unquoted attr: "onclick="`,
954 `<input type=button value= onclick=>`,
955 `html/template:z: "=" in unquoted attr: "onclick="`,
958 `<input type=button value= 1+1=2>`,
959 `html/template:z: "=" in unquoted attr: "1+1=2"`,
963 "html/template:z: \"`\" in unquoted attr: \"`foo\"",
966 `<a style=font:'Arial'>`,
967 `html/template:z: "'" in unquoted attr: "font:'Arial'"`,
971 `: expected space, attr name, or end of tag, but got "=foo>"`,
975 for _
, test
:= range tests
{
976 buf
:= new(bytes
.Buffer
)
977 tmpl
, err
:= New("z").Parse(test
.input
)
979 t
.Errorf("input=%q: unexpected parse error %s\n", test
.input
, err
)
982 err
= tmpl
.Execute(buf
, nil)
989 t
.Errorf("input=%q: unexpected error %q", test
.input
, got
)
993 if strings
.Index(got
, test
.err
) == -1 {
994 t
.Errorf("input=%q: error\n\t%q\ndoes not contain expected string\n\t%q", test
.input
, got
, test
.err
)
1000 func TestEscapeText(t
*testing
.T
) {
1014 // An orphaned "<" is OK.
1020 context
{state
: stateTag
},
1024 context
{state
: stateTag
},
1028 context
{state
: stateText
},
1032 context
{state
: stateAttrName
, attr
: attrURL
},
1036 context
{state
: stateAttrName
, attr
: attrScript
},
1040 context
{state
: stateAfterName
, attr
: attrURL
},
1044 context
{state
: stateBeforeValue
, attr
: attrStyle
},
1048 context
{state
: stateBeforeValue
, attr
: attrURL
},
1052 context
{state
: stateURL
, delim
: delimSpaceOrTagEnd
, urlPart
: urlPartPreQuery
},
1056 context
{state
: stateTag
},
1060 context
{state
: stateText
},
1064 context
{state
: stateText
},
1068 context
{state
: stateURL
, delim
: delimSingleQuote
},
1072 context
{state
: stateTag
},
1076 context
{state
: stateURL
, delim
: delimDoubleQuote
},
1080 context
{state
: stateTag
},
1084 context
{state
: stateAttr
, delim
: delimDoubleQuote
},
1088 context
{state
: stateURL
, delim
: delimSingleQuote
, urlPart
: urlPartPreQuery
},
1092 context
{state
: stateURL
, delim
: delimSingleQuote
, urlPart
: urlPartPreQuery
},
1096 context
{state
: stateURL
, delim
: delimSingleQuote
, urlPart
: urlPartPreQuery
},
1100 context
{state
: stateURL
, delim
: delimDoubleQuote
, urlPart
: urlPartPreQuery
},
1104 context
{state
: stateURL
, delim
: delimSingleQuote
, urlPart
: urlPartPreQuery
},
1108 context
{state
: stateURL
, delim
: delimDoubleQuote
, urlPart
: urlPartPreQuery
},
1112 context
{state
: stateURL
, delim
: delimDoubleQuote
, urlPart
: urlPartPreQuery
},
1116 context
{state
: stateURL
, delim
: delimSpaceOrTagEnd
, urlPart
: urlPartPreQuery
},
1120 context
{state
: stateText
},
1124 context
{state
: stateTag
},
1128 context
{state
: stateText
},
1131 `<input checked type="checkbox"`,
1132 context
{state
: stateTag
},
1136 context
{state
: stateJS
, delim
: delimDoubleQuote
},
1139 `<a onclick="//foo`,
1140 context
{state
: stateJSLineCmt
, delim
: delimDoubleQuote
},
1144 context
{state
: stateJS
, delim
: delimSingleQuote
},
1147 "<a onclick='//\r\n",
1148 context
{state
: stateJS
, delim
: delimSingleQuote
},
1151 "<a onclick='//\u2028",
1152 context
{state
: stateJS
, delim
: delimSingleQuote
},
1156 context
{state
: stateJSBlockCmt
, delim
: delimDoubleQuote
},
1160 context
{state
: stateJSBlockCmt
, delim
: delimDoubleQuote
},
1164 context
{state
: stateJS
, delim
: delimDoubleQuote
},
1167 `<a onkeypress=""`,
1168 context
{state
: stateJSDqStr
, delim
: delimDoubleQuote
},
1171 `<a onclick='"foo"`,
1172 context
{state
: stateJS
, delim
: delimSingleQuote
, jsCtx
: jsCtxDivOp
},
1175 `<a onclick='foo'`,
1176 context
{state
: stateJS
, delim
: delimSpaceOrTagEnd
, jsCtx
: jsCtxDivOp
},
1179 `<a onclick='foo`,
1180 context
{state
: stateJSSqStr
, delim
: delimSpaceOrTagEnd
},
1183 `<a onclick=""foo'`,
1184 context
{state
: stateJSDqStr
, delim
: delimDoubleQuote
},
1187 `<a onclick="'foo"`,
1188 context
{state
: stateJSSqStr
, delim
: delimDoubleQuote
},
1192 context
{state
: stateJSSqStr
, delim
: delimDoubleQuote
},
1196 context
{state
: stateJSRegexp
, delim
: delimDoubleQuote
},
1199 `<a onclick="'foo'`,
1200 context
{state
: stateJS
, delim
: delimDoubleQuote
, jsCtx
: jsCtxDivOp
},
1203 `<a onclick="'foo\'`,
1204 context
{state
: stateJSSqStr
, delim
: delimDoubleQuote
},
1207 `<a onclick="'foo\'`,
1208 context
{state
: stateJSSqStr
, delim
: delimDoubleQuote
},
1211 `<a onclick="/foo/`,
1212 context
{state
: stateJS
, delim
: delimDoubleQuote
, jsCtx
: jsCtxDivOp
},
1216 context
{state
: stateJS
, element
: elementScript
},
1219 `<a onclick="1 /foo`,
1220 context
{state
: stateJS
, delim
: delimDoubleQuote
, jsCtx
: jsCtxDivOp
},
1223 `<a onclick="1 /*c*/ /foo`,
1224 context
{state
: stateJS
, delim
: delimDoubleQuote
, jsCtx
: jsCtxDivOp
},
1227 `<a onclick="/foo[/]`,
1228 context
{state
: stateJSRegexp
, delim
: delimDoubleQuote
},
1231 `<a onclick="/foo\/`,
1232 context
{state
: stateJSRegexp
, delim
: delimDoubleQuote
},
1235 `<a onclick="/foo/`,
1236 context
{state
: stateJS
, delim
: delimDoubleQuote
, jsCtx
: jsCtxDivOp
},
1239 `<input checked style="`,
1240 context
{state
: stateCSS
, delim
: delimDoubleQuote
},
1244 context
{state
: stateCSSLineCmt
, delim
: delimDoubleQuote
},
1247 `<a style="//</script>`,
1248 context
{state
: stateCSSLineCmt
, delim
: delimDoubleQuote
},
1252 context
{state
: stateCSS
, delim
: delimSingleQuote
},
1256 context
{state
: stateCSS
, delim
: delimSingleQuote
},
1260 context
{state
: stateCSSBlockCmt
, delim
: delimDoubleQuote
},
1264 context
{state
: stateCSSBlockCmt
, delim
: delimDoubleQuote
},
1268 context
{state
: stateCSS
, delim
: delimDoubleQuote
},
1271 `<a style="background: '`,
1272 context
{state
: stateCSSSqStr
, delim
: delimDoubleQuote
},
1275 `<a style="background: "`,
1276 context
{state
: stateCSSDqStr
, delim
: delimDoubleQuote
},
1279 `<a style="background: '/foo?img=`,
1280 context
{state
: stateCSSSqStr
, delim
: delimDoubleQuote
, urlPart
: urlPartQueryOrFrag
},
1283 `<a style="background: '/`,
1284 context
{state
: stateCSSSqStr
, delim
: delimDoubleQuote
, urlPart
: urlPartPreQuery
},
1287 `<a style="background: url("/`,
1288 context
{state
: stateCSSDqURL
, delim
: delimDoubleQuote
, urlPart
: urlPartPreQuery
},
1291 `<a style="background: url('/`,
1292 context
{state
: stateCSSSqURL
, delim
: delimDoubleQuote
, urlPart
: urlPartPreQuery
},
1295 `<a style="background: url('/)`,
1296 context
{state
: stateCSSSqURL
, delim
: delimDoubleQuote
, urlPart
: urlPartPreQuery
},
1299 `<a style="background: url('/ `,
1300 context
{state
: stateCSSSqURL
, delim
: delimDoubleQuote
, urlPart
: urlPartPreQuery
},
1303 `<a style="background: url(/`,
1304 context
{state
: stateCSSURL
, delim
: delimDoubleQuote
, urlPart
: urlPartPreQuery
},
1307 `<a style="background: url( `,
1308 context
{state
: stateCSSURL
, delim
: delimDoubleQuote
},
1311 `<a style="background: url( /image?name=`,
1312 context
{state
: stateCSSURL
, delim
: delimDoubleQuote
, urlPart
: urlPartQueryOrFrag
},
1315 `<a style="background: url(x)`,
1316 context
{state
: stateCSS
, delim
: delimDoubleQuote
},
1319 `<a style="background: url('x'`,
1320 context
{state
: stateCSS
, delim
: delimDoubleQuote
},
1323 `<a style="background: url( x `,
1324 context
{state
: stateCSS
, delim
: delimDoubleQuote
},
1328 context
{state
: stateHTMLCmt
},
1332 context
{state
: stateHTMLCmt
},
1336 context
{state
: stateHTMLCmt
},
1340 context
{state
: stateText
},
1344 context
{state
: stateTag
, element
: elementScript
},
1348 context
{state
: stateTag
, element
: elementScript
},
1351 `<script src="foo.js" `,
1352 context
{state
: stateTag
, element
: elementScript
},
1355 `<script src='foo.js' `,
1356 context
{state
: stateTag
, element
: elementScript
},
1359 `<script type=text/javascript `,
1360 context
{state
: stateTag
, element
: elementScript
},
1364 context
{state
: stateJS
, jsCtx
: jsCtxDivOp
, element
: elementScript
},
1367 `<script>foo</script>`,
1368 context
{state
: stateText
},
1371 `<script>foo</script><!--`,
1372 context
{state
: stateHTMLCmt
},
1375 `<script>document.write("<p>foo</p>");`,
1376 context
{state
: stateJS
, element
: elementScript
},
1379 `<script>document.write("<p>foo<\/script>");`,
1380 context
{state
: stateJS
, element
: elementScript
},
1383 `<script>document.write("<script>alert(1)</script>");`,
1384 context
{state
: stateText
},
1388 context
{state
: stateJS
, element
: elementScript
},
1392 context
{state
: stateJS
, jsCtx
: jsCtxDivOp
, element
: elementScript
},
1396 context
{state
: stateRCDATA
, element
: elementTextarea
},
1399 `<textarea>value</TEXTAREA>`,
1400 context
{state
: stateText
},
1403 `<textarea name=html><b`,
1404 context
{state
: stateRCDATA
, element
: elementTextarea
},
1408 context
{state
: stateRCDATA
, element
: elementTitle
},
1412 context
{state
: stateCSS
, element
: elementStyle
},
1416 context
{state
: stateAttrName
, attr
: attrURL
},
1420 context
{state
: stateAttrName
, attr
: attrURL
},
1424 context
{state
: stateAttrName
, attr
: attrURL
},
1428 context
{state
: stateAttrName
},
1432 context
{state
: stateAttrName
, attr
: attrURL
},
1436 context
{state
: stateAttrName
, attr
: attrURL
},
1440 context
{state
: stateAttrName
, attr
: attrURL
},
1444 context
{state
: stateAttrName
},
1448 context
{state
: stateAttrName
, attr
: attrURL
},
1452 context
{state
: stateAttrName
, attr
: attrURL
},
1456 context
{state
: stateAttrName
, attr
: attrURL
},
1460 context
{state
: stateAttrName
},
1464 context
{state
: stateCSS
, delim
: delimSingleQuote
},
1468 context
{state
: stateTag
},
1471 `<svg:a svg:onclick="`,
1472 context
{state
: stateJS
, delim
: delimDoubleQuote
},
1476 for _
, test
:= range tests
{
1477 b
, e
:= []byte(test
.input
), newEscaper(nil)
1478 c
:= e
.escapeText(context
{}, &parse
.TextNode
{NodeType
: parse
.NodeText
, Text
: b
})
1479 if !test
.output
.eq(c
) {
1480 t
.Errorf("input %q: want context\n\t%v\ngot\n\t%v", test
.input
, test
.output
, c
)
1483 if test
.input
!= string(b
) {
1484 t
.Errorf("input %q: text node was modified: want %q got %q", test
.input
, test
.input
, b
)
1490 func TestEnsurePipelineContains(t
*testing
.T
) {
1492 input
, output
string
1512 ".X | html | urlquery",
1513 []string{"urlquery"},
1516 "{{.X | html | urlquery}}",
1517 ".X | html | urlquery",
1518 []string{"urlquery"},
1521 "{{.X | html | urlquery}}",
1522 ".X | html | urlquery",
1523 []string{"html", "urlquery"},
1526 "{{.X | html | urlquery}}",
1527 ".X | html | urlquery",
1531 "{{.X | urlquery}}",
1532 ".X | html | urlquery",
1533 []string{"html", "urlquery"},
1536 "{{.X | html | print}}",
1537 ".X | urlquery | html | print",
1538 []string{"urlquery", "html"},
1541 "{{($).X | html | print}}",
1542 "($).X | urlquery | html | print",
1543 []string{"urlquery", "html"},
1546 for i
, test
:= range tests
{
1547 tmpl
:= template
.Must(template
.New("test").Parse(test
.input
))
1548 action
, ok
:= (tmpl
.Tree
.Root
.Nodes
[0].(*parse
.ActionNode
))
1550 t
.Errorf("#%d: First node is not an action: %s", i
, test
.input
)
1554 ensurePipelineContains(pipe
, test
.ids
)
1555 got
:= pipe
.String()
1556 if got
!= test
.output
{
1557 t
.Errorf("#%d: %s, %v: want\n\t%s\ngot\n\t%s", i
, test
.input
, test
.ids
, test
.output
, got
)
1562 func TestEscapeErrorsNotIgnorable(t
*testing
.T
) {
1564 tmpl
, _
:= New("dangerous").Parse("<a")
1565 err
:= tmpl
.Execute(&b
, nil)
1567 t
.Errorf("Expected error")
1568 } else if b
.Len() != 0 {
1569 t
.Errorf("Emitted output despite escaping failure")
1573 func TestEscapeSetErrorsNotIgnorable(t
*testing
.T
) {
1575 tmpl
, err
:= New("root").Parse(`{{define "t"}}<a{{end}}`)
1577 t
.Errorf("failed to parse set: %q", err
)
1579 err
= tmpl
.ExecuteTemplate(&b
, "t", nil)
1581 t
.Errorf("Expected error")
1582 } else if b
.Len() != 0 {
1583 t
.Errorf("Emitted output despite escaping failure")
1587 func TestRedundantFuncs(t
*testing
.T
) {
1588 inputs
:= []interface{}{
1589 "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" +
1590 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
1591 ` !"#$%&'()*+,-./` +
1592 `0123456789:;<=>?` +
1593 `@ABCDEFGHIJKLMNO` +
1594 `PQRSTUVWXYZ[\]^_` +
1595 "`abcdefghijklmno" +
1596 "pqrstuvwxyz{|}~\x7f" +
1597 "\u00A0\u0100\u2028\u2029\ufeff\ufdec\ufffd\uffff\U0001D11E" +
1599 CSS(`a[href =~ "//example.com"]#foo`),
1600 HTML(`Hello, <b>World</b> &tc!`),
1601 HTMLAttr(` dir="ltr"`),
1602 JS(`c && alert("Hello, World!");`),
1603 JSStr(`Hello, World & O'Reilly\x21`),
1604 URL(`greeting=H%69&addressee=(World)`),
1607 for n0
, m
:= range redundantFuncs
{
1608 f0
:= funcMap
[n0
].(func(...interface{}) string)
1610 f1
:= funcMap
[n1
].(func(...interface{}) string)
1611 for _
, input
:= range inputs
{
1613 if got
:= f1(want
); want
!= got
{
1614 t
.Errorf("%s %s with %T %q: want\n\t%q,\ngot\n\t%q", n0
, n1
, input
, input
, want
, got
)
1621 func TestIndirectPrint(t
*testing
.T
) {
1627 tmpl
:= Must(New("t").Parse(`{{.}}`))
1628 var buf bytes
.Buffer
1629 err
:= tmpl
.Execute(&buf
, ap
)
1631 t
.Errorf("Unexpected error: %s", err
)
1632 } else if buf
.String() != "3" {
1633 t
.Errorf(`Expected "3"; got %q`, buf
.String())
1636 err
= tmpl
.Execute(&buf
, bpp
)
1638 t
.Errorf("Unexpected error: %s", err
)
1639 } else if buf
.String() != "hello" {
1640 t
.Errorf(`Expected "hello"; got %q`, buf
.String())
1644 // This is a test for issue 3272.
1645 func TestEmptyTemplate(t
*testing
.T
) {
1646 page
:= Must(New("page").ParseFiles(os
.DevNull
))
1647 if err
:= page
.ExecuteTemplate(os
.Stdout
, "page", "nothing"); err
== nil {
1648 t
.Fatal("expected error")
1654 func (Issue7379
) SomeMethod(x
int) string {
1655 return fmt
.Sprintf("<%d>", x
)
1658 // This is a test for issue 7379: type assertion error caused panic, and then
1659 // the code to handle the panic breaks escaping. It's hard to see the second
1660 // problem once the first is fixed, but its fix is trivial so we let that go. See
1661 // the discussion for issue 7379.
1662 func TestPipeToMethodIsEscaped(t
*testing
.T
) {
1663 tmpl
:= Must(New("x").Parse("<html>{{0 | .SomeMethod}}</html>\n"))
1664 tryExec
:= func() string {
1666 panicValue
:= recover()
1667 if panicValue
!= nil {
1668 t
.Errorf("panicked: %v\n", panicValue
)
1672 tmpl
.Execute(&b
, Issue7379(0))
1675 for i
:= 0; i
< 3; i
++ {
1677 const expect
= "<html><0></html>\n"
1679 t
.Errorf("expected %q got %q", expect
, str
)
1684 func BenchmarkEscapedExecute(b
*testing
.B
) {
1685 tmpl
:= Must(New("t").Parse(`<a onclick="alert('{{.}}')">{{.}}</a>`))
1686 var buf bytes
.Buffer
1688 for i
:= 0; i
< b
.N
; i
++ {
1689 tmpl
.Execute(&buf
, "foo & 'bar' & baz")