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 package template 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "fmt" 11 "os" 12 "strings" 13 "testing" 14 "text/template" 15 "text/template/parse" 16 ) 17 18 type badMarshaler struct{} 19 20 func (x *badMarshaler) MarshalJSON() ([]byte, error) { 21 // Keys in valid JSON must be double quoted as must all strings. 22 return []byte("{ foo: 'not quite valid JSON' }"), nil 23 } 24 25 type goodMarshaler struct{} 26 27 func (x *goodMarshaler) MarshalJSON() ([]byte, error) { 28 return []byte(`{ "<foo>": "O'Reilly" }`), nil 29 } 30 31 func TestEscape(t *testing.T) { 32 data := struct { 33 F, T bool 34 C, G, H string 35 A, E []string 36 B, M json.Marshaler 37 N int 38 Z *int 39 W HTML 40 }{ 41 F: false, 42 T: true, 43 C: "<Cincinatti>", 44 G: "<Goodbye>", 45 H: "<Hello>", 46 A: []string{"<a>", "<b>"}, 47 E: []string{}, 48 N: 42, 49 B: &badMarshaler{}, 50 M: &goodMarshaler{}, 51 Z: nil, 52 W: HTML(`¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`), 53 } 54 pdata := &data 55 56 tests := []struct { 57 name string 58 input string 59 output string 60 }{ 61 { 62 "if", 63 "{{if .T}}Hello{{end}}, {{.C}}!", 64 "Hello, <Cincinatti>!", 65 }, 66 { 67 "else", 68 "{{if .F}}{{.H}}{{else}}{{.G}}{{end}}!", 69 "<Goodbye>!", 70 }, 71 { 72 "overescaping1", 73 "Hello, {{.C | html}}!", 74 "Hello, <Cincinatti>!", 75 }, 76 { 77 "overescaping2", 78 "Hello, {{html .C}}!", 79 "Hello, <Cincinatti>!", 80 }, 81 { 82 "overescaping3", 83 "{{with .C}}{{$msg := .}}Hello, {{$msg}}!{{end}}", 84 "Hello, <Cincinatti>!", 85 }, 86 { 87 "assignment", 88 "{{if $x := .H}}{{$x}}{{end}}", 89 "<Hello>", 90 }, 91 { 92 "withBody", 93 "{{with .H}}{{.}}{{end}}", 94 "<Hello>", 95 }, 96 { 97 "withElse", 98 "{{with .E}}{{.}}{{else}}{{.H}}{{end}}", 99 "<Hello>", 100 }, 101 { 102 "rangeBody", 103 "{{range .A}}{{.}}{{end}}", 104 "<a><b>", 105 }, 106 { 107 "rangeElse", 108 "{{range .E}}{{.}}{{else}}{{.H}}{{end}}", 109 "<Hello>", 110 }, 111 { 112 "nonStringValue", 113 "{{.T}}", 114 "true", 115 }, 116 { 117 "constant", 118 `<a href="/search?q={{"'a<b'"}}">`, 119 `<a href="/search?q=%27a%3cb%27">`, 120 }, 121 { 122 "multipleAttrs", 123 "<a b=1 c={{.H}}>", 124 "<a b=1 c=<Hello>>", 125 }, 126 { 127 "urlStartRel", 128 `<a href='{{"/foo/bar?a=b&c=d"}}'>`, 129 `<a href='/foo/bar?a=b&c=d'>`, 130 }, 131 { 132 "urlStartAbsOk", 133 `<a href='{{"http://example.com/foo/bar?a=b&c=d"}}'>`, 134 `<a href='http://example.com/foo/bar?a=b&c=d'>`, 135 }, 136 { 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'>`, 140 }, 141 { 142 "pathRelativeURLStart", 143 `<a href="{{"/javascript:80/foo/bar"}}">`, 144 `<a href="/javascript:80/foo/bar">`, 145 }, 146 { 147 "dangerousURLStart", 148 `<a href='{{"javascript:alert(%22pwned%22)"}}'>`, 149 `<a href='#ZgotmplZ'>`, 150 }, 151 { 152 "dangerousURLStart2", 153 `<a href=' {{"javascript:alert(%22pwned%22)"}}'>`, 154 `<a href=' #ZgotmplZ'>`, 155 }, 156 { 157 "nonHierURL", 158 `<a href={{"mailto:Muhammed \"The Greatest\" Ali <m.ali (a] example.com>"}}>`, 159 `<a href=mailto:Muhammed%20%22The%20Greatest%22%20Ali%20%3cm.ali (a] example.com%3e>`, 160 }, 161 { 162 "urlPath", 163 `<a href='http://{{"javascript:80"}}/foo'>`, 164 `<a href='http://javascript:80/foo'>`, 165 }, 166 { 167 "urlQuery", 168 `<a href='/search?q={{.H}}'>`, 169 `<a href='/search?q=%3cHello%3e'>`, 170 }, 171 { 172 "urlFragment", 173 `<a href='/faq#{{.H}}'>`, 174 `<a href='/faq#%3cHello%3e'>`, 175 }, 176 { 177 "urlBranch", 178 `<a href="{{if .F}}/foo?a=b{{else}}/bar{{end}}">`, 179 `<a href="/bar">`, 180 }, 181 { 182 "urlBranchConflictMoot", 183 `<a href="{{if .T}}/foo?a={{else}}/bar#{{end}}{{.C}}">`, 184 `<a href="/foo?a=%3cCincinatti%3e">`, 185 }, 186 { 187 "jsStrValue", 188 "<button onclick='alert({{.H}})'>", 189 `<button onclick='alert("\u003cHello\u003e")'>`, 190 }, 191 { 192 "jsNumericValue", 193 "<button onclick='alert({{.N}})'>", 194 `<button onclick='alert( 42 )'>`, 195 }, 196 { 197 "jsBoolValue", 198 "<button onclick='alert({{.T}})'>", 199 `<button onclick='alert( true )'>`, 200 }, 201 { 202 "jsNilValue", 203 "<button onclick='alert(typeof{{.Z}})'>", 204 `<button onclick='alert(typeof null )'>`, 205 }, 206 { 207 "jsObjValue", 208 "<button onclick='alert({{.A}})'>", 209 `<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`, 210 }, 211 { 212 "jsObjValueScript", 213 "<script>alert({{.A}})</script>", 214 `<script>alert(["\u003ca\u003e","\u003cb\u003e"])</script>`, 215 }, 216 { 217 "jsObjValueNotOverEscaped", 218 "<button onclick='alert({{.A | html}})'>", 219 `<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`, 220 }, 221 { 222 "jsStr", 223 "<button onclick='alert("{{.H}}")'>", 224 `<button onclick='alert("\x3cHello\x3e")'>`, 225 }, 226 { 227 "badMarshaler", 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)'>`, 230 }, 231 { 232 "jsMarshaler", 233 `<button onclick='alert({{.M}})'>`, 234 `<button onclick='alert({"\u003cfoo\u003e":"O'Reilly"})'>`, 235 }, 236 { 237 "jsStrNotUnderEscaped", 238 "<button onclick='alert({{.C | urlquery}})'>", 239 // URL escaped, then quoted for JS. 240 `<button onclick='alert("%3CCincinatti%3E")'>`, 241 }, 242 { 243 "jsRe", 244 `<button onclick='alert(/{{"foo+bar"}}/.test(""))'>`, 245 `<button onclick='alert(/foo\x2bbar/.test(""))'>`, 246 }, 247 { 248 "jsReBlank", 249 `<script>alert(/{{""}}/.test(""));</script>`, 250 `<script>alert(/(?:)/.test(""));</script>`, 251 }, 252 { 253 "jsReAmbigOk", 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>`, 258 }, 259 { 260 "styleBidiKeywordPassed", 261 `<p style="dir: {{"ltr"}}">`, 262 `<p style="dir: ltr">`, 263 }, 264 { 265 "styleBidiPropNamePassed", 266 `<p style="border-{{"left"}}: 0; border-{{"right"}}: 1in">`, 267 `<p style="border-left: 0; border-right: 1in">`, 268 }, 269 { 270 "styleExpressionBlocked", 271 `<p style="width: {{"expression(alert(1337))"}}">`, 272 `<p style="width: ZgotmplZ">`, 273 }, 274 { 275 "styleTagSelectorPassed", 276 `<style>{{"p"}} { color: pink }</style>`, 277 `<style>p { color: pink }</style>`, 278 }, 279 { 280 "styleIDPassed", 281 `<style>p{{"#my-ID"}} { font: Arial }</style>`, 282 `<style>p#my-ID { font: Arial }</style>`, 283 }, 284 { 285 "styleClassPassed", 286 `<style>p{{".my_class"}} { font: Arial }</style>`, 287 `<style>p.my_class { font: Arial }</style>`, 288 }, 289 { 290 "styleQuantityPassed", 291 `<a style="left: {{"2em"}}; top: {{0}}">`, 292 `<a style="left: 2em; top: 0">`, 293 }, 294 { 295 "stylePctPassed", 296 `<table style=width:{{"100%"}}>`, 297 `<table style=width:100%>`, 298 }, 299 { 300 "styleColorPassed", 301 `<p style="color: {{"#8ff"}}; background: {{"#000"}}">`, 302 `<p style="color: #8ff; background: #000">`, 303 }, 304 { 305 "styleObfuscatedExpressionBlocked", 306 `<p style="width: {{" e\\78preS\x00Sio/**/n(alert(1337))"}}">`, 307 `<p style="width: ZgotmplZ">`, 308 }, 309 { 310 "styleMozBindingBlocked", 311 `<p style="{{"-moz-binding(alert(1337))"}}: ...">`, 312 `<p style="ZgotmplZ: ...">`, 313 }, 314 { 315 "styleObfuscatedMozBindingBlocked", 316 `<p style="{{" -mo\\7a-B\x00I/**/nding(alert(1337))"}}: ...">`, 317 `<p style="ZgotmplZ: ...">`, 318 }, 319 { 320 "styleFontNameString", 321 `<p style='font-family: "{{"Times New Roman"}}"'>`, 322 `<p style='font-family: "Times New Roman"'>`, 323 }, 324 { 325 "styleFontNameString", 326 `<p style='font-family: "{{"Times New Roman"}}", "{{"sans-serif"}}"'>`, 327 `<p style='font-family: "Times New Roman", "sans-serif"'>`, 328 }, 329 { 330 "styleFontNameUnquoted", 331 `<p style='font-family: {{"Times New Roman"}}'>`, 332 `<p style='font-family: Times New Roman'>`, 333 }, 334 { 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)">`, 338 }, 339 { 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')">`, 343 }, 344 { 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'">`, 348 }, 349 { 350 "styleURLBadProtocolBlocked", 351 `<a style="background: url('{{"javascript:alert(1337)"}}')">`, 352 `<a style="background: url('#ZgotmplZ')">`, 353 }, 354 { 355 "styleStrBadProtocolBlocked", 356 `<a style="background: '{{"vbscript:alert(1337)"}}'">`, 357 `<a style="background: '#ZgotmplZ'">`, 358 }, 359 { 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 '">`, 364 }, 365 { 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')">`, 369 }, 370 { 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'">`, 374 }, 375 { 376 "styleURLEncodedForHTMLInAttr", 377 `<a style="background: url('{{"/search?img=foo&size=icon"}}')">`, 378 `<a style="background: url('/search?img=foo&size=icon')">`, 379 }, 380 { 381 "styleURLNotEncodedForHTMLInCdata", 382 `<style>body { background: url('{{"/search?img=foo&size=icon"}}') }</style>`, 383 `<style>body { background: url('/search?img=foo&size=icon') }</style>`, 384 }, 385 { 386 "styleURLMixedCase", 387 `<p style="background: URL(#{{.H}})">`, 388 `<p style="background: URL(#%3cHello%3e)">`, 389 }, 390 { 391 "stylePropertyPairPassed", 392 `<a style='{{"color: red"}}'>`, 393 `<a style='color: red'>`, 394 }, 395 { 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 \\"">`, 399 }, 400 { 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''">`, 404 }, 405 { 406 "HTML comment", 407 "<b>Hello, <!-- name of world -->{{.C}}</b>", 408 "<b>Hello, <Cincinatti></b>", 409 }, 410 { 411 "HTML comment not first < in text node.", 412 "<<!-- -->!--", 413 "<!--", 414 }, 415 { 416 "HTML normalization 1", 417 "a < b", 418 "a < b", 419 }, 420 { 421 "HTML normalization 2", 422 "a << b", 423 "a << b", 424 }, 425 { 426 "HTML normalization 3", 427 "a<<!-- --><!-- -->b", 428 "a<b", 429 }, 430 { 431 "HTML doctype not normalized", 432 "<!DOCTYPE html>Hello, World!", 433 "<!DOCTYPE html>Hello, World!", 434 }, 435 { 436 "HTML doctype not case-insensitive", 437 "<!doCtYPE htMl>Hello, World!", 438 "<!doCtYPE htMl>Hello, World!", 439 }, 440 { 441 "No doctype injection", 442 `<!{{"DOCTYPE"}}`, 443 "<!DOCTYPE", 444 }, 445 { 446 "Split HTML comment", 447 "<b>Hello, <!-- name of {{if .T}}city -->{{.C}}{{else}}world -->{{.W}}{{end}}</b>", 448 "<b>Hello, <Cincinatti></b>", 449 }, 450 { 451 "JS line comment", 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>", 456 }, 457 { 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 463 // code invalid. 464 "<script>for (;;) { if (c()) break\n" + 465 "foo( true );}</script>", 466 }, 467 { 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 474 // code invalid. 475 "<script>for (;;) {\n" + 476 "if (c()) break foo;" + 477 "x( true );}</script>", 478 }, 479 { 480 "JS block comment flush with mathematical division", 481 "<script>var a/*b*//c\nd</script>", 482 "<script>var a /c\nd</script>", 483 }, 484 { 485 "JS mixed comments", 486 "<script>var a/*b*///c\nd</script>", 487 "<script>var a \nd</script>", 488 }, 489 { 490 "CSS comments", 491 "<style>p// paragraph\n" + 492 `{border: 1px/* color */{{"#00f"}}}</style>`, 493 "<style>p\n" + 494 "{border: 1px #00f}</style>", 495 }, 496 { 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() */">`, 502 }, 503 { 504 "JS attr line comment", 505 `<a onclick="// alert({{.G}})">`, 506 `<a onclick="// alert()">`, 507 }, 508 { 509 "CSS attr block comment", 510 `<a style="/* color: {{.H}} */">`, 511 `<a style="/* color: */">`, 512 }, 513 { 514 "CSS attr line comment", 515 `<a style="// color: {{.G}}">`, 516 `<a style="// color: ">`, 517 }, 518 { 519 "HTML substitution commented out", 520 "<p><!-- {{.H}} --></p>", 521 "<p></p>", 522 }, 523 { 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='/**///'>", 527 }, 528 { 529 "typed HTML in text", 530 `{{.W}}`, 531 `¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`, 532 }, 533 { 534 "typed HTML in attribute", 535 `<div title="{{.W}}">`, 536 `<div title="¡Hello, O'World!">`, 537 }, 538 { 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!")">`, 542 }, 543 { 544 "typed HTML in RCDATA", 545 `<textarea>{{.W}}</textarea>`, 546 `<textarea>¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!</textarea>`, 547 }, 548 { 549 "range in textarea", 550 "<textarea>{{range .A}}{{.}}{{end}}</textarea>", 551 "<textarea><a><b></textarea>", 552 }, 553 { 554 "No tag injection", 555 `{{"10$"}}<{{"script src,evil.org/pwnd.js"}}...`, 556 `10$<script src,evil.org/pwnd.js...`, 557 }, 558 { 559 "No comment injection", 560 `<{{"!--"}}`, 561 `<!--`, 562 }, 563 { 564 "No RCDATA end tag injection", 565 `<textarea><{{"/textarea "}}...</textarea>`, 566 `<textarea></textarea ...</textarea>`, 567 }, 568 { 569 "optional attrs", 570 `<img class="{{"iconClass"}}"` + 571 `{{if .T}} id="{{"<iconId>"}}"{{end}}` + 572 // Double quotes inside if/else. 573 ` src=` + 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. 580 ` alt="` + 581 `{{if .T}}{{"<alt>"}}` + 582 `{{else}}{{if .F}}{{"<title>"}}{{end}}` + 583 `{{end}}"` + 584 `>`, 585 `<img class="iconClass" id="<iconId>" src="?%3ciconPath%3e"title="<title>" alt="<alt>">`, 586 }, 587 { 588 "conditional valueless attr name", 589 `<input{{if .T}} checked{{end}} name=n>`, 590 `<input checked name=n>`, 591 }, 592 { 593 "conditional dynamic valueless attr name 1", 594 `<input{{if .T}} {{"checked"}}{{end}} name=n>`, 595 `<input checked name=n>`, 596 }, 597 { 598 "conditional dynamic valueless attr name 2", 599 `<input {{if .T}}{{"checked"}} {{end}}name=n>`, 600 `<input checked name=n>`, 601 }, 602 { 603 "dynamic attribute name", 604 `<img on{{"load"}}="alert({{"loaded"}})">`, 605 // Treated as JS since quotes are inserted. 606 `<img onload="alert("loaded")">`, 607 }, 608 { 609 "bad dynamic attribute name 1", 610 // Allow checked, selected, disabled, but not JS or 611 // CSS attributes. 612 `<input {{"onchange"}}="{{"doEvil()"}}">`, 613 `<input ZgotmplZ="doEvil()">`, 614 }, 615 { 616 "bad dynamic attribute name 2", 617 `<div {{"sTyle"}}="{{"color: expression(alert(1337))"}}">`, 618 `<div ZgotmplZ="color: expression(alert(1337))">`, 619 }, 620 { 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()">`, 625 }, 626 { 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?">`, 632 }, 633 { 634 "dynamic element name", 635 `<h{{3}}><table><t{{"head"}}>...</h{{3}}>`, 636 `<h3><table><thead>...</h3>`, 637 }, 638 { 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>`, 652 }, 653 } 654 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) 661 continue 662 } 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) 666 continue 667 } 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) 670 continue 671 } 672 b.Reset() 673 if err := tmpl.Execute(b, pdata); err != nil { 674 t.Errorf("%s: template execution failed for pointer: %s", test.name, err) 675 continue 676 } 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) 679 continue 680 } 681 if tmpl.Tree != tmpl.text.Tree { 682 t.Errorf("%s: tree mismatch", test.name) 683 continue 684 } 685 } 686 } 687 688 func TestEscapeSet(t *testing.T) { 689 type dataItem struct { 690 Children []*dataItem 691 X string 692 } 693 694 data := dataItem{ 695 Children: []*dataItem{ 696 {X: "foo"}, 697 {X: "<bar>"}, 698 { 699 Children: []*dataItem{ 700 {X: "baz"}, 701 }, 702 }, 703 }, 704 } 705 706 tests := []struct { 707 inputs map[string]string 708 want string 709 }{ 710 // The trivial set. 711 { 712 map[string]string{ 713 "main": ``, 714 }, 715 ``, 716 }, 717 // A template called in the start context. 718 { 719 map[string]string{ 720 "main": `Hello, {{template "helper"}}!`, 721 // Not a valid top level HTML template. 722 // "<b" is not a full tag. 723 "helper": `{{"<World>"}}`, 724 }, 725 `Hello, <World>!`, 726 }, 727 // A template called in a context other than the start. 728 { 729 map[string]string{ 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`, 734 }, 735 `<a onclick='a = "\u003ca\u003e"<b;'>`, 736 }, 737 // A recursive template that ends in its start context. 738 { 739 map[string]string{ 740 "main": `{{range .Children}}{{template "main" .}}{{else}}{{.X}} {{end}}`, 741 }, 742 `foo <bar> baz `, 743 }, 744 // A recursive helper template that ends in its start context. 745 { 746 map[string]string{ 747 "main": `{{template "helper" .}}`, 748 "helper": `{{if .Children}}<ul>{{range .Children}}<li>{{template "main" .}}</li>{{end}}</ul>{{else}}{{.X}}{{end}}`, 749 }, 750 `<ul><li>foo</li><li><bar></li><li><ul><li>baz</li></ul></li></ul>`, 751 }, 752 // Co-recursive templates that end in its start context. 753 { 754 map[string]string{ 755 "main": `<blockquote>{{range .Children}}{{template "helper" .}}{{end}}</blockquote>`, 756 "helper": `{{if .Children}}{{template "main" .}}{{else}}{{.X}}<br>{{end}}`, 757 }, 758 `<blockquote>foo<br><bar><br><blockquote>baz<br></blockquote></blockquote>`, 759 }, 760 // A template that is called in two different contexts. 761 { 762 map[string]string{ 763 "main": `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`, 764 "helper": `{{11}} of {{"<100>"}}`, 765 }, 766 `<button onclick="title='11 of \x3c100\x3e'; ...">11 of <100></button>`, 767 }, 768 // A non-recursive template that ends in a different context. 769 // helper starts in jsCtxRegexp and ends in jsCtxDivOp. 770 { 771 map[string]string{ 772 "main": `<script>var x={{template "helper"}}/{{"42"}};</script>`, 773 "helper": "{{126}}", 774 }, 775 `<script>var x= 126 /"42";</script>`, 776 }, 777 // A recursive template that ends in a similar context. 778 { 779 map[string]string{ 780 "main": `<script>var x=[{{template "countdown" 4}}];</script>`, 781 "countdown": `{{.}}{{if .}},{{template "countdown" . | pred}}{{end}}`, 782 }, 783 `<script>var x=[ 4 , 3 , 2 , 1 , 0 ];</script>`, 784 }, 785 // A recursive template that ends in a different context. 786 /* 787 { 788 map[string]string{ 789 "main": `<a href="/foo{{template "helper" .}}">`, 790 "helper": `{{if .Children}}{{range .Children}}{{template "helper" .}}{{end}}{{else}}?x={{.X}}{{end}}`, 791 }, 792 `<a href="/foo?x=foo?x=%3cbar%3e?x=baz">`, 793 }, 794 */ 795 } 796 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) { 800 if len(a) == 1 { 801 if i, _ := a[0].(int); i > 0 { 802 return i - 1, nil 803 } 804 } 805 return nil, fmt.Errorf("undefined pred(%v)", a) 806 }} 807 808 for _, test := range tests { 809 source := "" 810 for name, body := range test.inputs { 811 source += fmt.Sprintf("{{define %q}}%s{{end}} ", name, body) 812 } 813 tmpl, err := New("root").Funcs(fns).Parse(source) 814 if err != nil { 815 t.Errorf("error parsing %q: %v", source, err) 816 continue 817 } 818 var b bytes.Buffer 819 820 if err := tmpl.ExecuteTemplate(&b, "main", data); err != nil { 821 t.Errorf("%q executing %v", err.Error(), tmpl.Lookup("main")) 822 continue 823 } 824 if got := b.String(); test.want != got { 825 t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got) 826 } 827 } 828 829 } 830 831 func TestErrors(t *testing.T) { 832 tests := []struct { 833 input string 834 err string 835 }{ 836 // Non-error cases. 837 { 838 "{{if .Cond}}<a>{{else}}<b>{{end}}", 839 "", 840 }, 841 { 842 "{{if .Cond}}<a>{{end}}", 843 "", 844 }, 845 { 846 "{{if .Cond}}{{else}}<b>{{end}}", 847 "", 848 }, 849 { 850 "{{with .Cond}}<div>{{end}}", 851 "", 852 }, 853 { 854 "{{range .Items}}<a>{{end}}", 855 "", 856 }, 857 { 858 "<a href='/foo?{{range .Items}}&{{.K}}={{.V}}{{end}}'>", 859 "", 860 }, 861 // Error cases. 862 { 863 "{{if .Cond}}<a{{end}}", 864 "z:1:5: {{if}} branches", 865 }, 866 { 867 "{{if .Cond}}\n{{else}}\n<a{{end}}", 868 "z:1:5: {{if}} branches", 869 }, 870 { 871 // Missing quote in the else branch. 872 `{{if .Cond}}<a href="foo">{{else}}<a href="bar>{{end}}`, 873 "z:1:5: {{if}} branches", 874 }, 875 { 876 // Different kind of attribute: href implies a URL. 877 "<a {{if .Cond}}href='{{else}}title='{{end}}{{.X}}'>", 878 "z:1:8: {{if}} branches", 879 }, 880 { 881 "\n{{with .X}}<a{{end}}", 882 "z:2:7: {{with}} branches", 883 }, 884 { 885 "\n{{with .X}}<a>{{else}}<a{{end}}", 886 "z:2:7: {{with}} branches", 887 }, 888 { 889 "{{range .Items}}<a{{end}}", 890 `z:1: on range loop re-entry: "<" in attribute name: "<a"`, 891 }, 892 { 893 "\n{{range .Items}} x='<a{{end}}", 894 "z:2:8: on range loop re-entry: {{range}} branches", 895 }, 896 { 897 "<a b=1 c={{.H}}", 898 "z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd", 899 }, 900 { 901 "<script>foo();", 902 "z: ends in a non-text context: {stateJS", 903 }, 904 { 905 `<a href="{{if .F}}/foo?a={{else}}/bar/{{end}}{{.H}}">`, 906 "z:1:47: {{.H}} appears in an ambiguous URL context", 907 }, 908 { 909 `<a onclick="alert('Hello \`, 910 `unfinished escape sequence in JS string: "Hello \\"`, 911 }, 912 { 913 `<a onclick='alert("Hello\, World\`, 914 `unfinished escape sequence in JS string: "Hello\\, World\\"`, 915 }, 916 { 917 `<a onclick='alert(/x+\`, 918 `unfinished escape sequence in JS string: "x+\\"`, 919 }, 920 { 921 `<a onclick="/foo[\]/`, 922 `unfinished JS regexp charset: "foo[\\]/"`, 923 }, 924 { 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: "/-"`, 932 }, 933 { 934 `{{template "foo"}}`, 935 "z:1:11: no such template \"foo\"", 936 }, 937 { 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"`, 942 }, 943 { 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`, 948 }, 949 { 950 `<input type=button value=onclick=>`, 951 `html/template:z: "=" in unquoted attr: "onclick="`, 952 }, 953 { 954 `<input type=button value= onclick=>`, 955 `html/template:z: "=" in unquoted attr: "onclick="`, 956 }, 957 { 958 `<input type=button value= 1+1=2>`, 959 `html/template:z: "=" in unquoted attr: "1+1=2"`, 960 }, 961 { 962 "<a class=`foo>", 963 "html/template:z: \"`\" in unquoted attr: \"`foo\"", 964 }, 965 { 966 `<a style=font:'Arial'>`, 967 `html/template:z: "'" in unquoted attr: "font:'Arial'"`, 968 }, 969 { 970 `<a=foo>`, 971 `: expected space, attr name, or end of tag, but got "=foo>"`, 972 }, 973 } 974 975 for _, test := range tests { 976 buf := new(bytes.Buffer) 977 tmpl, err := New("z").Parse(test.input) 978 if err != nil { 979 t.Errorf("input=%q: unexpected parse error %s\n", test.input, err) 980 continue 981 } 982 err = tmpl.Execute(buf, nil) 983 var got string 984 if err != nil { 985 got = err.Error() 986 } 987 if test.err == "" { 988 if got != "" { 989 t.Errorf("input=%q: unexpected error %q", test.input, got) 990 } 991 continue 992 } 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) 995 continue 996 } 997 // Check that we get the same error if we call Execute again. 998 if err := tmpl.Execute(buf, nil); err == nil || err.Error() != got { 999 t.Errorf("input=%q: unexpected error on second call %q", test.input, err) 1000 1001 } 1002 } 1003 } 1004 1005 func TestEscapeText(t *testing.T) { 1006 tests := []struct { 1007 input string 1008 output context 1009 }{ 1010 { 1011 ``, 1012 context{}, 1013 }, 1014 { 1015 `Hello, World!`, 1016 context{}, 1017 }, 1018 { 1019 // An orphaned "<" is OK. 1020 `I <3 Ponies!`, 1021 context{}, 1022 }, 1023 { 1024 `<a`, 1025 context{state: stateTag}, 1026 }, 1027 { 1028 `<a `, 1029 context{state: stateTag}, 1030 }, 1031 { 1032 `<a>`, 1033 context{state: stateText}, 1034 }, 1035 { 1036 `<a href`, 1037 context{state: stateAttrName, attr: attrURL}, 1038 }, 1039 { 1040 `<a on`, 1041 context{state: stateAttrName, attr: attrScript}, 1042 }, 1043 { 1044 `<a href `, 1045 context{state: stateAfterName, attr: attrURL}, 1046 }, 1047 { 1048 `<a style = `, 1049 context{state: stateBeforeValue, attr: attrStyle}, 1050 }, 1051 { 1052 `<a href=`, 1053 context{state: stateBeforeValue, attr: attrURL}, 1054 }, 1055 { 1056 `<a href=x`, 1057 context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery}, 1058 }, 1059 { 1060 `<a href=x `, 1061 context{state: stateTag}, 1062 }, 1063 { 1064 `<a href=>`, 1065 context{state: stateText}, 1066 }, 1067 { 1068 `<a href=x>`, 1069 context{state: stateText}, 1070 }, 1071 { 1072 `<a href ='`, 1073 context{state: stateURL, delim: delimSingleQuote}, 1074 }, 1075 { 1076 `<a href=''`, 1077 context{state: stateTag}, 1078 }, 1079 { 1080 `<a href= "`, 1081 context{state: stateURL, delim: delimDoubleQuote}, 1082 }, 1083 { 1084 `<a href=""`, 1085 context{state: stateTag}, 1086 }, 1087 { 1088 `<a title="`, 1089 context{state: stateAttr, delim: delimDoubleQuote}, 1090 }, 1091 { 1092 `<a HREF='http:`, 1093 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, 1094 }, 1095 { 1096 `<a Href='/`, 1097 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, 1098 }, 1099 { 1100 `<a href='"`, 1101 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, 1102 }, 1103 { 1104 `<a href="'`, 1105 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1106 }, 1107 { 1108 `<a href=''`, 1109 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery}, 1110 }, 1111 { 1112 `<a href=""`, 1113 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1114 }, 1115 { 1116 `<a href=""`, 1117 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1118 }, 1119 { 1120 `<a href="`, 1121 context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery}, 1122 }, 1123 { 1124 `<img alt="1">`, 1125 context{state: stateText}, 1126 }, 1127 { 1128 `<img alt="1>"`, 1129 context{state: stateTag}, 1130 }, 1131 { 1132 `<img alt="1>">`, 1133 context{state: stateText}, 1134 }, 1135 { 1136 `<input checked type="checkbox"`, 1137 context{state: stateTag}, 1138 }, 1139 { 1140 `<a onclick="`, 1141 context{state: stateJS, delim: delimDoubleQuote}, 1142 }, 1143 { 1144 `<a onclick="//foo`, 1145 context{state: stateJSLineCmt, delim: delimDoubleQuote}, 1146 }, 1147 { 1148 "<a onclick='//\n", 1149 context{state: stateJS, delim: delimSingleQuote}, 1150 }, 1151 { 1152 "<a onclick='//\r\n", 1153 context{state: stateJS, delim: delimSingleQuote}, 1154 }, 1155 { 1156 "<a onclick='//\u2028", 1157 context{state: stateJS, delim: delimSingleQuote}, 1158 }, 1159 { 1160 `<a onclick="/*`, 1161 context{state: stateJSBlockCmt, delim: delimDoubleQuote}, 1162 }, 1163 { 1164 `<a onclick="/*/`, 1165 context{state: stateJSBlockCmt, delim: delimDoubleQuote}, 1166 }, 1167 { 1168 `<a onclick="/**/`, 1169 context{state: stateJS, delim: delimDoubleQuote}, 1170 }, 1171 { 1172 `<a onkeypress=""`, 1173 context{state: stateJSDqStr, delim: delimDoubleQuote}, 1174 }, 1175 { 1176 `<a onclick='"foo"`, 1177 context{state: stateJS, delim: delimSingleQuote, jsCtx: jsCtxDivOp}, 1178 }, 1179 { 1180 `<a onclick='foo'`, 1181 context{state: stateJS, delim: delimSpaceOrTagEnd, jsCtx: jsCtxDivOp}, 1182 }, 1183 { 1184 `<a onclick='foo`, 1185 context{state: stateJSSqStr, delim: delimSpaceOrTagEnd}, 1186 }, 1187 { 1188 `<a onclick=""foo'`, 1189 context{state: stateJSDqStr, delim: delimDoubleQuote}, 1190 }, 1191 { 1192 `<a onclick="'foo"`, 1193 context{state: stateJSSqStr, delim: delimDoubleQuote}, 1194 }, 1195 { 1196 `<A ONCLICK="'`, 1197 context{state: stateJSSqStr, delim: delimDoubleQuote}, 1198 }, 1199 { 1200 `<a onclick="/`, 1201 context{state: stateJSRegexp, delim: delimDoubleQuote}, 1202 }, 1203 { 1204 `<a onclick="'foo'`, 1205 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, 1206 }, 1207 { 1208 `<a onclick="'foo\'`, 1209 context{state: stateJSSqStr, delim: delimDoubleQuote}, 1210 }, 1211 { 1212 `<a onclick="'foo\'`, 1213 context{state: stateJSSqStr, delim: delimDoubleQuote}, 1214 }, 1215 { 1216 `<a onclick="/foo/`, 1217 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, 1218 }, 1219 { 1220 `<script>/foo/ /=`, 1221 context{state: stateJS, element: elementScript}, 1222 }, 1223 { 1224 `<a onclick="1 /foo`, 1225 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, 1226 }, 1227 { 1228 `<a onclick="1 /*c*/ /foo`, 1229 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, 1230 }, 1231 { 1232 `<a onclick="/foo[/]`, 1233 context{state: stateJSRegexp, delim: delimDoubleQuote}, 1234 }, 1235 { 1236 `<a onclick="/foo\/`, 1237 context{state: stateJSRegexp, delim: delimDoubleQuote}, 1238 }, 1239 { 1240 `<a onclick="/foo/`, 1241 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp}, 1242 }, 1243 { 1244 `<input checked style="`, 1245 context{state: stateCSS, delim: delimDoubleQuote}, 1246 }, 1247 { 1248 `<a style="//`, 1249 context{state: stateCSSLineCmt, delim: delimDoubleQuote}, 1250 }, 1251 { 1252 `<a style="//</script>`, 1253 context{state: stateCSSLineCmt, delim: delimDoubleQuote}, 1254 }, 1255 { 1256 "<a style='//\n", 1257 context{state: stateCSS, delim: delimSingleQuote}, 1258 }, 1259 { 1260 "<a style='//\r", 1261 context{state: stateCSS, delim: delimSingleQuote}, 1262 }, 1263 { 1264 `<a style="/*`, 1265 context{state: stateCSSBlockCmt, delim: delimDoubleQuote}, 1266 }, 1267 { 1268 `<a style="/*/`, 1269 context{state: stateCSSBlockCmt, delim: delimDoubleQuote}, 1270 }, 1271 { 1272 `<a style="/**/`, 1273 context{state: stateCSS, delim: delimDoubleQuote}, 1274 }, 1275 { 1276 `<a style="background: '`, 1277 context{state: stateCSSSqStr, delim: delimDoubleQuote}, 1278 }, 1279 { 1280 `<a style="background: "`, 1281 context{state: stateCSSDqStr, delim: delimDoubleQuote}, 1282 }, 1283 { 1284 `<a style="background: '/foo?img=`, 1285 context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag}, 1286 }, 1287 { 1288 `<a style="background: '/`, 1289 context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1290 }, 1291 { 1292 `<a style="background: url("/`, 1293 context{state: stateCSSDqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1294 }, 1295 { 1296 `<a style="background: url('/`, 1297 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1298 }, 1299 { 1300 `<a style="background: url('/)`, 1301 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1302 }, 1303 { 1304 `<a style="background: url('/ `, 1305 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1306 }, 1307 { 1308 `<a style="background: url(/`, 1309 context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery}, 1310 }, 1311 { 1312 `<a style="background: url( `, 1313 context{state: stateCSSURL, delim: delimDoubleQuote}, 1314 }, 1315 { 1316 `<a style="background: url( /image?name=`, 1317 context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag}, 1318 }, 1319 { 1320 `<a style="background: url(x)`, 1321 context{state: stateCSS, delim: delimDoubleQuote}, 1322 }, 1323 { 1324 `<a style="background: url('x'`, 1325 context{state: stateCSS, delim: delimDoubleQuote}, 1326 }, 1327 { 1328 `<a style="background: url( x `, 1329 context{state: stateCSS, delim: delimDoubleQuote}, 1330 }, 1331 { 1332 `<!-- foo`, 1333 context{state: stateHTMLCmt}, 1334 }, 1335 { 1336 `<!-->`, 1337 context{state: stateHTMLCmt}, 1338 }, 1339 { 1340 `<!--->`, 1341 context{state: stateHTMLCmt}, 1342 }, 1343 { 1344 `<!-- foo -->`, 1345 context{state: stateText}, 1346 }, 1347 { 1348 `<script`, 1349 context{state: stateTag, element: elementScript}, 1350 }, 1351 { 1352 `<script `, 1353 context{state: stateTag, element: elementScript}, 1354 }, 1355 { 1356 `<script src="foo.js" `, 1357 context{state: stateTag, element: elementScript}, 1358 }, 1359 { 1360 `<script src='foo.js' `, 1361 context{state: stateTag, element: elementScript}, 1362 }, 1363 { 1364 `<script type=text/javascript `, 1365 context{state: stateTag, element: elementScript}, 1366 }, 1367 { 1368 `<script>foo`, 1369 context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, 1370 }, 1371 { 1372 `<script>foo</script>`, 1373 context{state: stateText}, 1374 }, 1375 { 1376 `<script>foo</script><!--`, 1377 context{state: stateHTMLCmt}, 1378 }, 1379 { 1380 `<script>document.write("<p>foo</p>");`, 1381 context{state: stateJS, element: elementScript}, 1382 }, 1383 { 1384 `<script>document.write("<p>foo<\/script>");`, 1385 context{state: stateJS, element: elementScript}, 1386 }, 1387 { 1388 `<script>document.write("<script>alert(1)</script>");`, 1389 context{state: stateText}, 1390 }, 1391 { 1392 `<Script>`, 1393 context{state: stateJS, element: elementScript}, 1394 }, 1395 { 1396 `<SCRIPT>foo`, 1397 context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, 1398 }, 1399 { 1400 `<textarea>value`, 1401 context{state: stateRCDATA, element: elementTextarea}, 1402 }, 1403 { 1404 `<textarea>value</TEXTAREA>`, 1405 context{state: stateText}, 1406 }, 1407 { 1408 `<textarea name=html><b`, 1409 context{state: stateRCDATA, element: elementTextarea}, 1410 }, 1411 { 1412 `<title>value`, 1413 context{state: stateRCDATA, element: elementTitle}, 1414 }, 1415 { 1416 `<style>value`, 1417 context{state: stateCSS, element: elementStyle}, 1418 }, 1419 { 1420 `<a xlink:href`, 1421 context{state: stateAttrName, attr: attrURL}, 1422 }, 1423 { 1424 `<a xmlns`, 1425 context{state: stateAttrName, attr: attrURL}, 1426 }, 1427 { 1428 `<a xmlns:foo`, 1429 context{state: stateAttrName, attr: attrURL}, 1430 }, 1431 { 1432 `<a xmlnsxyz`, 1433 context{state: stateAttrName}, 1434 }, 1435 { 1436 `<a data-url`, 1437 context{state: stateAttrName, attr: attrURL}, 1438 }, 1439 { 1440 `<a data-iconUri`, 1441 context{state: stateAttrName, attr: attrURL}, 1442 }, 1443 { 1444 `<a data-urlItem`, 1445 context{state: stateAttrName, attr: attrURL}, 1446 }, 1447 { 1448 `<a g:`, 1449 context{state: stateAttrName}, 1450 }, 1451 { 1452 `<a g:url`, 1453 context{state: stateAttrName, attr: attrURL}, 1454 }, 1455 { 1456 `<a g:iconUri`, 1457 context{state: stateAttrName, attr: attrURL}, 1458 }, 1459 { 1460 `<a g:urlItem`, 1461 context{state: stateAttrName, attr: attrURL}, 1462 }, 1463 { 1464 `<a g:value`, 1465 context{state: stateAttrName}, 1466 }, 1467 { 1468 `<a svg:style='`, 1469 context{state: stateCSS, delim: delimSingleQuote}, 1470 }, 1471 { 1472 `<svg:font-face`, 1473 context{state: stateTag}, 1474 }, 1475 { 1476 `<svg:a svg:onclick="`, 1477 context{state: stateJS, delim: delimDoubleQuote}, 1478 }, 1479 } 1480 1481 for _, test := range tests { 1482 b, e := []byte(test.input), newEscaper(nil) 1483 c := e.escapeText(context{}, &parse.TextNode{NodeType: parse.NodeText, Text: b}) 1484 if !test.output.eq(c) { 1485 t.Errorf("input %q: want context\n\t%v\ngot\n\t%v", test.input, test.output, c) 1486 continue 1487 } 1488 if test.input != string(b) { 1489 t.Errorf("input %q: text node was modified: want %q got %q", test.input, test.input, b) 1490 continue 1491 } 1492 } 1493 } 1494 1495 func TestEnsurePipelineContains(t *testing.T) { 1496 tests := []struct { 1497 input, output string 1498 ids []string 1499 }{ 1500 { 1501 "{{.X}}", 1502 ".X", 1503 []string{}, 1504 }, 1505 { 1506 "{{.X | html}}", 1507 ".X | html", 1508 []string{}, 1509 }, 1510 { 1511 "{{.X}}", 1512 ".X | html", 1513 []string{"html"}, 1514 }, 1515 { 1516 "{{.X | html}}", 1517 ".X | html | urlquery", 1518 []string{"urlquery"}, 1519 }, 1520 { 1521 "{{.X | html | urlquery}}", 1522 ".X | html | urlquery", 1523 []string{"urlquery"}, 1524 }, 1525 { 1526 "{{.X | html | urlquery}}", 1527 ".X | html | urlquery", 1528 []string{"html", "urlquery"}, 1529 }, 1530 { 1531 "{{.X | html | urlquery}}", 1532 ".X | html | urlquery", 1533 []string{"html"}, 1534 }, 1535 { 1536 "{{.X | urlquery}}", 1537 ".X | html | urlquery", 1538 []string{"html", "urlquery"}, 1539 }, 1540 { 1541 "{{.X | html | print}}", 1542 ".X | urlquery | html | print", 1543 []string{"urlquery", "html"}, 1544 }, 1545 { 1546 "{{($).X | html | print}}", 1547 "($).X | urlquery | html | print", 1548 []string{"urlquery", "html"}, 1549 }, 1550 { 1551 "{{.X | print 2 | .f 3}}", 1552 ".X | print 2 | .f 3 | urlquery | html", 1553 []string{"urlquery", "html"}, 1554 }, 1555 { 1556 "{{.X | html | print 2 | .f 3}}", 1557 ".X | urlquery | html | print 2 | .f 3", 1558 []string{"urlquery", "html"}, 1559 }, 1560 { 1561 // covering issue 10801 1562 "{{.X | js.x }}", 1563 ".X | js.x | urlquery | html", 1564 []string{"urlquery", "html"}, 1565 }, 1566 { 1567 // covering issue 10801 1568 "{{.X | (print 12 | js).x }}", 1569 ".X | (print 12 | js).x | urlquery | html", 1570 []string{"urlquery", "html"}, 1571 }, 1572 } 1573 for i, test := range tests { 1574 tmpl := template.Must(template.New("test").Parse(test.input)) 1575 action, ok := (tmpl.Tree.Root.Nodes[0].(*parse.ActionNode)) 1576 if !ok { 1577 t.Errorf("#%d: First node is not an action: %s", i, test.input) 1578 continue 1579 } 1580 pipe := action.Pipe 1581 ensurePipelineContains(pipe, test.ids) 1582 got := pipe.String() 1583 if got != test.output { 1584 t.Errorf("#%d: %s, %v: want\n\t%s\ngot\n\t%s", i, test.input, test.ids, test.output, got) 1585 } 1586 } 1587 } 1588 1589 func TestEscapeMalformedPipelines(t *testing.T) { 1590 tests := []string{ 1591 "{{ 0 | $ }}", 1592 "{{ 0 | $ | urlquery }}", 1593 "{{ 0 | $ | urlquery | html }}", 1594 "{{ 0 | (nil) }}", 1595 "{{ 0 | (nil) | html }}", 1596 "{{ 0 | (nil) | html | urlquery }}", 1597 } 1598 for _, test := range tests { 1599 var b bytes.Buffer 1600 tmpl, err := New("test").Parse(test) 1601 if err != nil { 1602 t.Errorf("failed to parse set: %q", err) 1603 } 1604 err = tmpl.Execute(&b, nil) 1605 if err == nil { 1606 t.Errorf("Expected error for %q", test) 1607 } 1608 } 1609 } 1610 1611 func TestEscapeErrorsNotIgnorable(t *testing.T) { 1612 var b bytes.Buffer 1613 tmpl, _ := New("dangerous").Parse("<a") 1614 err := tmpl.Execute(&b, nil) 1615 if err == nil { 1616 t.Errorf("Expected error") 1617 } else if b.Len() != 0 { 1618 t.Errorf("Emitted output despite escaping failure") 1619 } 1620 } 1621 1622 func TestEscapeSetErrorsNotIgnorable(t *testing.T) { 1623 var b bytes.Buffer 1624 tmpl, err := New("root").Parse(`{{define "t"}}<a{{end}}`) 1625 if err != nil { 1626 t.Errorf("failed to parse set: %q", err) 1627 } 1628 err = tmpl.ExecuteTemplate(&b, "t", nil) 1629 if err == nil { 1630 t.Errorf("Expected error") 1631 } else if b.Len() != 0 { 1632 t.Errorf("Emitted output despite escaping failure") 1633 } 1634 } 1635 1636 func TestRedundantFuncs(t *testing.T) { 1637 inputs := []interface{}{ 1638 "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + 1639 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + 1640 ` !"#$%&'()*+,-./` + 1641 `0123456789:;<=>?` + 1642 `@ABCDEFGHIJKLMNO` + 1643 `PQRSTUVWXYZ[\]^_` + 1644 "`abcdefghijklmno" + 1645 "pqrstuvwxyz{|}~\x7f" + 1646 "\u00A0\u0100\u2028\u2029\ufeff\ufdec\ufffd\uffff\U0001D11E" + 1647 "&%22\\", 1648 CSS(`a[href =~ "//example.com"]#foo`), 1649 HTML(`Hello, <b>World</b> &tc!`), 1650 HTMLAttr(` dir="ltr"`), 1651 JS(`c && alert("Hello, World!");`), 1652 JSStr(`Hello, World & O'Reilly\x21`), 1653 URL(`greeting=H%69&addressee=(World)`), 1654 } 1655 1656 for n0, m := range redundantFuncs { 1657 f0 := funcMap[n0].(func(...interface{}) string) 1658 for n1 := range m { 1659 f1 := funcMap[n1].(func(...interface{}) string) 1660 for _, input := range inputs { 1661 want := f0(input) 1662 if got := f1(want); want != got { 1663 t.Errorf("%s %s with %T %q: want\n\t%q,\ngot\n\t%q", n0, n1, input, input, want, got) 1664 } 1665 } 1666 } 1667 } 1668 } 1669 1670 func TestIndirectPrint(t *testing.T) { 1671 a := 3 1672 ap := &a 1673 b := "hello" 1674 bp := &b 1675 bpp := &bp 1676 tmpl := Must(New("t").Parse(`{{.}}`)) 1677 var buf bytes.Buffer 1678 err := tmpl.Execute(&buf, ap) 1679 if err != nil { 1680 t.Errorf("Unexpected error: %s", err) 1681 } else if buf.String() != "3" { 1682 t.Errorf(`Expected "3"; got %q`, buf.String()) 1683 } 1684 buf.Reset() 1685 err = tmpl.Execute(&buf, bpp) 1686 if err != nil { 1687 t.Errorf("Unexpected error: %s", err) 1688 } else if buf.String() != "hello" { 1689 t.Errorf(`Expected "hello"; got %q`, buf.String()) 1690 } 1691 } 1692 1693 // This is a test for issue 3272. 1694 func TestEmptyTemplate(t *testing.T) { 1695 page := Must(New("page").ParseFiles(os.DevNull)) 1696 if err := page.ExecuteTemplate(os.Stdout, "page", "nothing"); err == nil { 1697 t.Fatal("expected error") 1698 } 1699 } 1700 1701 type Issue7379 int 1702 1703 func (Issue7379) SomeMethod(x int) string { 1704 return fmt.Sprintf("<%d>", x) 1705 } 1706 1707 // This is a test for issue 7379: type assertion error caused panic, and then 1708 // the code to handle the panic breaks escaping. It's hard to see the second 1709 // problem once the first is fixed, but its fix is trivial so we let that go. See 1710 // the discussion for issue 7379. 1711 func TestPipeToMethodIsEscaped(t *testing.T) { 1712 tmpl := Must(New("x").Parse("<html>{{0 | .SomeMethod}}</html>\n")) 1713 tryExec := func() string { 1714 defer func() { 1715 panicValue := recover() 1716 if panicValue != nil { 1717 t.Errorf("panicked: %v\n", panicValue) 1718 } 1719 }() 1720 var b bytes.Buffer 1721 tmpl.Execute(&b, Issue7379(0)) 1722 return b.String() 1723 } 1724 for i := 0; i < 3; i++ { 1725 str := tryExec() 1726 const expect = "<html><0></html>\n" 1727 if str != expect { 1728 t.Errorf("expected %q got %q", expect, str) 1729 } 1730 } 1731 } 1732 1733 // Unlike text/template, html/template crashed if given an incomplete 1734 // template, that is, a template that had been named but not given any content. 1735 // This is issue #10204. 1736 func TestErrorOnUndefined(t *testing.T) { 1737 tmpl := New("undefined") 1738 1739 err := tmpl.Execute(nil, nil) 1740 if err == nil { 1741 t.Error("expected error") 1742 } 1743 if !strings.Contains(err.Error(), "incomplete") { 1744 t.Errorf("expected error about incomplete template; got %s", err) 1745 } 1746 } 1747 1748 func BenchmarkEscapedExecute(b *testing.B) { 1749 tmpl := Must(New("t").Parse(`<a onclick="alert('{{.}}')">{{.}}</a>`)) 1750 var buf bytes.Buffer 1751 b.ResetTimer() 1752 for i := 0; i < b.N; i++ { 1753 tmpl.Execute(&buf, "foo & 'bar' & baz") 1754 buf.Reset() 1755 } 1756 } 1757