github.com/mattn/go@v0.0.0-20171011075504-07f7db3ea99f/src/html/template/escape_test.go (about) 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@example.com>"}}>`, 159 `<a href=mailto:Muhammed%20%22The%20Greatest%22%20Ali%20%3cm.ali@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 contain 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 TestEscapeMap(t *testing.T) { 689 data := map[string]string{ 690 "html": `<h1>Hi!</h1>`, 691 "urlquery": `http://www.foo.com/index.html?title=main`, 692 } 693 for _, test := range [...]struct { 694 desc, input, output string 695 }{ 696 // covering issue 20323 697 { 698 "field with predefined escaper name 1", 699 `{{.html | print}}`, 700 `<h1>Hi!</h1>`, 701 }, 702 // covering issue 20323 703 { 704 "field with predefined escaper name 2", 705 `{{.urlquery | print}}`, 706 `http://www.foo.com/index.html?title=main`, 707 }, 708 } { 709 tmpl := Must(New("").Parse(test.input)) 710 b := new(bytes.Buffer) 711 if err := tmpl.Execute(b, data); err != nil { 712 t.Errorf("%s: template execution failed: %s", test.desc, err) 713 continue 714 } 715 if w, g := test.output, b.String(); w != g { 716 t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.desc, w, g) 717 continue 718 } 719 } 720 } 721 722 func TestEscapeSet(t *testing.T) { 723 type dataItem struct { 724 Children []*dataItem 725 X string 726 } 727 728 data := dataItem{ 729 Children: []*dataItem{ 730 {X: "foo"}, 731 {X: "<bar>"}, 732 { 733 Children: []*dataItem{ 734 {X: "baz"}, 735 }, 736 }, 737 }, 738 } 739 740 tests := []struct { 741 inputs map[string]string 742 want string 743 }{ 744 // The trivial set. 745 { 746 map[string]string{ 747 "main": ``, 748 }, 749 ``, 750 }, 751 // A template called in the start context. 752 { 753 map[string]string{ 754 "main": `Hello, {{template "helper"}}!`, 755 // Not a valid top level HTML template. 756 // "<b" is not a full tag. 757 "helper": `{{"<World>"}}`, 758 }, 759 `Hello, <World>!`, 760 }, 761 // A template called in a context other than the start. 762 { 763 map[string]string{ 764 "main": `<a onclick='a = {{template "helper"}};'>`, 765 // Not a valid top level HTML template. 766 // "<b" is not a full tag. 767 "helper": `{{"<a>"}}<b`, 768 }, 769 `<a onclick='a = "\u003ca\u003e"<b;'>`, 770 }, 771 // A recursive template that ends in its start context. 772 { 773 map[string]string{ 774 "main": `{{range .Children}}{{template "main" .}}{{else}}{{.X}} {{end}}`, 775 }, 776 `foo <bar> baz `, 777 }, 778 // A recursive helper template that ends in its start context. 779 { 780 map[string]string{ 781 "main": `{{template "helper" .}}`, 782 "helper": `{{if .Children}}<ul>{{range .Children}}<li>{{template "main" .}}</li>{{end}}</ul>{{else}}{{.X}}{{end}}`, 783 }, 784 `<ul><li>foo</li><li><bar></li><li><ul><li>baz</li></ul></li></ul>`, 785 }, 786 // Co-recursive templates that end in its start context. 787 { 788 map[string]string{ 789 "main": `<blockquote>{{range .Children}}{{template "helper" .}}{{end}}</blockquote>`, 790 "helper": `{{if .Children}}{{template "main" .}}{{else}}{{.X}}<br>{{end}}`, 791 }, 792 `<blockquote>foo<br><bar><br><blockquote>baz<br></blockquote></blockquote>`, 793 }, 794 // A template that is called in two different contexts. 795 { 796 map[string]string{ 797 "main": `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`, 798 "helper": `{{11}} of {{"<100>"}}`, 799 }, 800 `<button onclick="title='11 of \x3c100\x3e'; ...">11 of <100></button>`, 801 }, 802 // A non-recursive template that ends in a different context. 803 // helper starts in jsCtxRegexp and ends in jsCtxDivOp. 804 { 805 map[string]string{ 806 "main": `<script>var x={{template "helper"}}/{{"42"}};</script>`, 807 "helper": "{{126}}", 808 }, 809 `<script>var x= 126 /"42";</script>`, 810 }, 811 // A recursive template that ends in a similar context. 812 { 813 map[string]string{ 814 "main": `<script>var x=[{{template "countdown" 4}}];</script>`, 815 "countdown": `{{.}}{{if .}},{{template "countdown" . | pred}}{{end}}`, 816 }, 817 `<script>var x=[ 4 , 3 , 2 , 1 , 0 ];</script>`, 818 }, 819 // A recursive template that ends in a different context. 820 /* 821 { 822 map[string]string{ 823 "main": `<a href="/foo{{template "helper" .}}">`, 824 "helper": `{{if .Children}}{{range .Children}}{{template "helper" .}}{{end}}{{else}}?x={{.X}}{{end}}`, 825 }, 826 `<a href="/foo?x=foo?x=%3cbar%3e?x=baz">`, 827 }, 828 */ 829 } 830 831 // pred is a template function that returns the predecessor of a 832 // natural number for testing recursive templates. 833 fns := FuncMap{"pred": func(a ...interface{}) (interface{}, error) { 834 if len(a) == 1 { 835 if i, _ := a[0].(int); i > 0 { 836 return i - 1, nil 837 } 838 } 839 return nil, fmt.Errorf("undefined pred(%v)", a) 840 }} 841 842 for _, test := range tests { 843 source := "" 844 for name, body := range test.inputs { 845 source += fmt.Sprintf("{{define %q}}%s{{end}} ", name, body) 846 } 847 tmpl, err := New("root").Funcs(fns).Parse(source) 848 if err != nil { 849 t.Errorf("error parsing %q: %v", source, err) 850 continue 851 } 852 var b bytes.Buffer 853 854 if err := tmpl.ExecuteTemplate(&b, "main", data); err != nil { 855 t.Errorf("%q executing %v", err.Error(), tmpl.Lookup("main")) 856 continue 857 } 858 if got := b.String(); test.want != got { 859 t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got) 860 } 861 } 862 863 } 864 865 func TestErrors(t *testing.T) { 866 tests := []struct { 867 input string 868 err string 869 }{ 870 // Non-error cases. 871 { 872 "{{if .Cond}}<a>{{else}}<b>{{end}}", 873 "", 874 }, 875 { 876 "{{if .Cond}}<a>{{end}}", 877 "", 878 }, 879 { 880 "{{if .Cond}}{{else}}<b>{{end}}", 881 "", 882 }, 883 { 884 "{{with .Cond}}<div>{{end}}", 885 "", 886 }, 887 { 888 "{{range .Items}}<a>{{end}}", 889 "", 890 }, 891 { 892 "<a href='/foo?{{range .Items}}&{{.K}}={{.V}}{{end}}'>", 893 "", 894 }, 895 // Error cases. 896 { 897 "{{if .Cond}}<a{{end}}", 898 "z:1:5: {{if}} branches", 899 }, 900 { 901 "{{if .Cond}}\n{{else}}\n<a{{end}}", 902 "z:1:5: {{if}} branches", 903 }, 904 { 905 // Missing quote in the else branch. 906 `{{if .Cond}}<a href="foo">{{else}}<a href="bar>{{end}}`, 907 "z:1:5: {{if}} branches", 908 }, 909 { 910 // Different kind of attribute: href implies a URL. 911 "<a {{if .Cond}}href='{{else}}title='{{end}}{{.X}}'>", 912 "z:1:8: {{if}} branches", 913 }, 914 { 915 "\n{{with .X}}<a{{end}}", 916 "z:2:7: {{with}} branches", 917 }, 918 { 919 "\n{{with .X}}<a>{{else}}<a{{end}}", 920 "z:2:7: {{with}} branches", 921 }, 922 { 923 "{{range .Items}}<a{{end}}", 924 `z:1: on range loop re-entry: "<" in attribute name: "<a"`, 925 }, 926 { 927 "\n{{range .Items}} x='<a{{end}}", 928 "z:2:8: on range loop re-entry: {{range}} branches", 929 }, 930 { 931 "<a b=1 c={{.H}}", 932 "z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd", 933 }, 934 { 935 "<script>foo();", 936 "z: ends in a non-text context: {stateJS", 937 }, 938 { 939 `<a href="{{if .F}}/foo?a={{else}}/bar/{{end}}{{.H}}">`, 940 "z:1:47: {{.H}} appears in an ambiguous context within a URL", 941 }, 942 { 943 `<a onclick="alert('Hello \`, 944 `unfinished escape sequence in JS string: "Hello \\"`, 945 }, 946 { 947 `<a onclick='alert("Hello\, World\`, 948 `unfinished escape sequence in JS string: "Hello\\, World\\"`, 949 }, 950 { 951 `<a onclick='alert(/x+\`, 952 `unfinished escape sequence in JS string: "x+\\"`, 953 }, 954 { 955 `<a onclick="/foo[\]/`, 956 `unfinished JS regexp charset: "foo[\\]/"`, 957 }, 958 { 959 // It is ambiguous whether 1.5 should be 1\.5 or 1.5. 960 // Either `var x = 1/- 1.5 /i.test(x)` 961 // where `i.test(x)` is a method call of reference i, 962 // or `/-1\.5/i.test(x)` which is a method call on a 963 // case insensitive regular expression. 964 `<script>{{if false}}var x = 1{{end}}/-{{"1.5"}}/i.test(x)</script>`, 965 `'/' could start a division or regexp: "/-"`, 966 }, 967 { 968 `{{template "foo"}}`, 969 "z:1:11: no such template \"foo\"", 970 }, 971 { 972 `<div{{template "y"}}>` + 973 // Illegal starting in stateTag but not in stateText. 974 `{{define "y"}} foo<b{{end}}`, 975 `"<" in attribute name: " foo<b"`, 976 }, 977 { 978 `<script>reverseList = [{{template "t"}}]</script>` + 979 // Missing " after recursive call. 980 `{{define "t"}}{{if .Tail}}{{template "t" .Tail}}{{end}}{{.Head}}",{{end}}`, 981 `: cannot compute output context for template t$htmltemplate_stateJS_elementScript`, 982 }, 983 { 984 `<input type=button value=onclick=>`, 985 `html/template:z: "=" in unquoted attr: "onclick="`, 986 }, 987 { 988 `<input type=button value= onclick=>`, 989 `html/template:z: "=" in unquoted attr: "onclick="`, 990 }, 991 { 992 `<input type=button value= 1+1=2>`, 993 `html/template:z: "=" in unquoted attr: "1+1=2"`, 994 }, 995 { 996 "<a class=`foo>", 997 "html/template:z: \"`\" in unquoted attr: \"`foo\"", 998 }, 999 { 1000 `<a style=font:'Arial'>`, 1001 `html/template:z: "'" in unquoted attr: "font:'Arial'"`, 1002 }, 1003 { 1004 `<a=foo>`, 1005 `: expected space, attr name, or end of tag, but got "=foo>"`, 1006 }, 1007 { 1008 `Hello, {{. | urlquery | print}}!`, 1009 // urlquery is disallowed if it is not the last command in the pipeline. 1010 `predefined escaper "urlquery" disallowed in template`, 1011 }, 1012 { 1013 `Hello, {{. | html | print}}!`, 1014 // html is disallowed if it is not the last command in the pipeline. 1015 `predefined escaper "html" disallowed in template`, 1016 }, 1017 { 1018 `Hello, {{html . | print}}!`, 1019 // A direct call to html is disallowed if it is not the last command in the pipeline. 1020 `predefined escaper "html" disallowed in template`, 1021 }, 1022 { 1023 `<div class={{. | html}}>Hello<div>`, 1024 // html is disallowed in a pipeline that is in an unquoted attribute context, 1025 // even if it is the last command in the pipeline. 1026 `predefined escaper "html" disallowed in template`, 1027 }, 1028 { 1029 `Hello, {{. | urlquery | html}}!`, 1030 // html is allowed since it is the last command in the pipeline, but urlquery is not. 1031 `predefined escaper "urlquery" disallowed in template`, 1032 }, 1033 } 1034 for _, test := range tests { 1035 buf := new(bytes.Buffer) 1036 tmpl, err := New("z").Parse(test.input) 1037 if err != nil { 1038 t.Errorf("input=%q: unexpected parse error %s\n", test.input, err) 1039 continue 1040 } 1041 err = tmpl.Execute(buf, nil) 1042 var got string 1043 if err != nil { 1044 got = err.Error() 1045 } 1046 if test.err == "" { 1047 if got != "" { 1048 t.Errorf("input=%q: unexpected error %q", test.input, got) 1049 } 1050 continue 1051 } 1052 if !strings.Contains(got, test.err) { 1053 t.Errorf("input=%q: error\n\t%q\ndoes not contain expected string\n\t%q", test.input, got, test.err) 1054 continue 1055 } 1056 // Check that we get the same error if we call Execute again. 1057 if err := tmpl.Execute(buf, nil); err == nil || err.Error() != got { 1058 t.Errorf("input=%q: unexpected error on second call %q", test.input, err) 1059 1060 } 1061 } 1062 } 1063 1064 func TestEscapeText(t *testing.T) { 1065 tests := []struct { 1066 input string 1067 output context 1068 }{ 1069 { 1070 ``, 1071 context{}, 1072 }, 1073 { 1074 `Hello, World!`, 1075 context{}, 1076 }, 1077 { 1078 // An orphaned "<" is OK. 1079 `I <3 Ponies!`, 1080 context{}, 1081 }, 1082 { 1083 `<a`, 1084 context{state: stateTag}, 1085 }, 1086 { 1087 `<a `, 1088 context{state: stateTag}, 1089 }, 1090 { 1091 `<a>`, 1092 context{state: stateText}, 1093 }, 1094 { 1095 `<a href`, 1096 context{state: stateAttrName, attr: attrURL}, 1097 }, 1098 { 1099 `<a on`, 1100 context{state: stateAttrName, attr: attrScript}, 1101 }, 1102 { 1103 `<a href `, 1104 context{state: stateAfterName, attr: attrURL}, 1105 }, 1106 { 1107 `<a style = `, 1108 context{state: stateBeforeValue, attr: attrStyle}, 1109 }, 1110 { 1111 `<a href=`, 1112 context{state: stateBeforeValue, attr: attrURL}, 1113 }, 1114 { 1115 `<a href=x`, 1116 context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery, attr: attrURL}, 1117 }, 1118 { 1119 `<a href=x `, 1120 context{state: stateTag}, 1121 }, 1122 { 1123 `<a href=>`, 1124 context{state: stateText}, 1125 }, 1126 { 1127 `<a href=x>`, 1128 context{state: stateText}, 1129 }, 1130 { 1131 `<a href ='`, 1132 context{state: stateURL, delim: delimSingleQuote, attr: attrURL}, 1133 }, 1134 { 1135 `<a href=''`, 1136 context{state: stateTag}, 1137 }, 1138 { 1139 `<a href= "`, 1140 context{state: stateURL, delim: delimDoubleQuote, attr: attrURL}, 1141 }, 1142 { 1143 `<a href=""`, 1144 context{state: stateTag}, 1145 }, 1146 { 1147 `<a title="`, 1148 context{state: stateAttr, delim: delimDoubleQuote}, 1149 }, 1150 { 1151 `<a HREF='http:`, 1152 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1153 }, 1154 { 1155 `<a Href='/`, 1156 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1157 }, 1158 { 1159 `<a href='"`, 1160 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1161 }, 1162 { 1163 `<a href="'`, 1164 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1165 }, 1166 { 1167 `<a href=''`, 1168 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1169 }, 1170 { 1171 `<a href=""`, 1172 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1173 }, 1174 { 1175 `<a href=""`, 1176 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1177 }, 1178 { 1179 `<a href="`, 1180 context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery, attr: attrURL}, 1181 }, 1182 { 1183 `<img alt="1">`, 1184 context{state: stateText}, 1185 }, 1186 { 1187 `<img alt="1>"`, 1188 context{state: stateTag}, 1189 }, 1190 { 1191 `<img alt="1>">`, 1192 context{state: stateText}, 1193 }, 1194 { 1195 `<input checked type="checkbox"`, 1196 context{state: stateTag}, 1197 }, 1198 { 1199 `<a onclick="`, 1200 context{state: stateJS, delim: delimDoubleQuote, attr: attrScript}, 1201 }, 1202 { 1203 `<a onclick="//foo`, 1204 context{state: stateJSLineCmt, delim: delimDoubleQuote, attr: attrScript}, 1205 }, 1206 { 1207 "<a onclick='//\n", 1208 context{state: stateJS, delim: delimSingleQuote, attr: attrScript}, 1209 }, 1210 { 1211 "<a onclick='//\r\n", 1212 context{state: stateJS, delim: delimSingleQuote, attr: attrScript}, 1213 }, 1214 { 1215 "<a onclick='//\u2028", 1216 context{state: stateJS, delim: delimSingleQuote, attr: attrScript}, 1217 }, 1218 { 1219 `<a onclick="/*`, 1220 context{state: stateJSBlockCmt, delim: delimDoubleQuote, attr: attrScript}, 1221 }, 1222 { 1223 `<a onclick="/*/`, 1224 context{state: stateJSBlockCmt, delim: delimDoubleQuote, attr: attrScript}, 1225 }, 1226 { 1227 `<a onclick="/**/`, 1228 context{state: stateJS, delim: delimDoubleQuote, attr: attrScript}, 1229 }, 1230 { 1231 `<a onkeypress=""`, 1232 context{state: stateJSDqStr, delim: delimDoubleQuote, attr: attrScript}, 1233 }, 1234 { 1235 `<a onclick='"foo"`, 1236 context{state: stateJS, delim: delimSingleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1237 }, 1238 { 1239 `<a onclick='foo'`, 1240 context{state: stateJS, delim: delimSpaceOrTagEnd, jsCtx: jsCtxDivOp, attr: attrScript}, 1241 }, 1242 { 1243 `<a onclick='foo`, 1244 context{state: stateJSSqStr, delim: delimSpaceOrTagEnd, attr: attrScript}, 1245 }, 1246 { 1247 `<a onclick=""foo'`, 1248 context{state: stateJSDqStr, delim: delimDoubleQuote, attr: attrScript}, 1249 }, 1250 { 1251 `<a onclick="'foo"`, 1252 context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, 1253 }, 1254 { 1255 `<A ONCLICK="'`, 1256 context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, 1257 }, 1258 { 1259 `<a onclick="/`, 1260 context{state: stateJSRegexp, delim: delimDoubleQuote, attr: attrScript}, 1261 }, 1262 { 1263 `<a onclick="'foo'`, 1264 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1265 }, 1266 { 1267 `<a onclick="'foo\'`, 1268 context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, 1269 }, 1270 { 1271 `<a onclick="'foo\'`, 1272 context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, 1273 }, 1274 { 1275 `<a onclick="/foo/`, 1276 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1277 }, 1278 { 1279 `<script>/foo/ /=`, 1280 context{state: stateJS, element: elementScript}, 1281 }, 1282 { 1283 `<a onclick="1 /foo`, 1284 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1285 }, 1286 { 1287 `<a onclick="1 /*c*/ /foo`, 1288 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1289 }, 1290 { 1291 `<a onclick="/foo[/]`, 1292 context{state: stateJSRegexp, delim: delimDoubleQuote, attr: attrScript}, 1293 }, 1294 { 1295 `<a onclick="/foo\/`, 1296 context{state: stateJSRegexp, delim: delimDoubleQuote, attr: attrScript}, 1297 }, 1298 { 1299 `<a onclick="/foo/`, 1300 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1301 }, 1302 { 1303 `<input checked style="`, 1304 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, 1305 }, 1306 { 1307 `<a style="//`, 1308 context{state: stateCSSLineCmt, delim: delimDoubleQuote, attr: attrStyle}, 1309 }, 1310 { 1311 `<a style="//</script>`, 1312 context{state: stateCSSLineCmt, delim: delimDoubleQuote, attr: attrStyle}, 1313 }, 1314 { 1315 "<a style='//\n", 1316 context{state: stateCSS, delim: delimSingleQuote, attr: attrStyle}, 1317 }, 1318 { 1319 "<a style='//\r", 1320 context{state: stateCSS, delim: delimSingleQuote, attr: attrStyle}, 1321 }, 1322 { 1323 `<a style="/*`, 1324 context{state: stateCSSBlockCmt, delim: delimDoubleQuote, attr: attrStyle}, 1325 }, 1326 { 1327 `<a style="/*/`, 1328 context{state: stateCSSBlockCmt, delim: delimDoubleQuote, attr: attrStyle}, 1329 }, 1330 { 1331 `<a style="/**/`, 1332 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, 1333 }, 1334 { 1335 `<a style="background: '`, 1336 context{state: stateCSSSqStr, delim: delimDoubleQuote, attr: attrStyle}, 1337 }, 1338 { 1339 `<a style="background: "`, 1340 context{state: stateCSSDqStr, delim: delimDoubleQuote, attr: attrStyle}, 1341 }, 1342 { 1343 `<a style="background: '/foo?img=`, 1344 context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag, attr: attrStyle}, 1345 }, 1346 { 1347 `<a style="background: '/`, 1348 context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1349 }, 1350 { 1351 `<a style="background: url("/`, 1352 context{state: stateCSSDqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1353 }, 1354 { 1355 `<a style="background: url('/`, 1356 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1357 }, 1358 { 1359 `<a style="background: url('/)`, 1360 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1361 }, 1362 { 1363 `<a style="background: url('/ `, 1364 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1365 }, 1366 { 1367 `<a style="background: url(/`, 1368 context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1369 }, 1370 { 1371 `<a style="background: url( `, 1372 context{state: stateCSSURL, delim: delimDoubleQuote, attr: attrStyle}, 1373 }, 1374 { 1375 `<a style="background: url( /image?name=`, 1376 context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag, attr: attrStyle}, 1377 }, 1378 { 1379 `<a style="background: url(x)`, 1380 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, 1381 }, 1382 { 1383 `<a style="background: url('x'`, 1384 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, 1385 }, 1386 { 1387 `<a style="background: url( x `, 1388 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, 1389 }, 1390 { 1391 `<!-- foo`, 1392 context{state: stateHTMLCmt}, 1393 }, 1394 { 1395 `<!-->`, 1396 context{state: stateHTMLCmt}, 1397 }, 1398 { 1399 `<!--->`, 1400 context{state: stateHTMLCmt}, 1401 }, 1402 { 1403 `<!-- foo -->`, 1404 context{state: stateText}, 1405 }, 1406 { 1407 `<script`, 1408 context{state: stateTag, element: elementScript}, 1409 }, 1410 { 1411 `<script `, 1412 context{state: stateTag, element: elementScript}, 1413 }, 1414 { 1415 `<script src="foo.js" `, 1416 context{state: stateTag, element: elementScript}, 1417 }, 1418 { 1419 `<script src='foo.js' `, 1420 context{state: stateTag, element: elementScript}, 1421 }, 1422 { 1423 `<script type=text/javascript `, 1424 context{state: stateTag, element: elementScript}, 1425 }, 1426 { 1427 `<script>`, 1428 context{state: stateJS, jsCtx: jsCtxRegexp, element: elementScript}, 1429 }, 1430 { 1431 `<script>foo`, 1432 context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, 1433 }, 1434 { 1435 `<script>foo</script>`, 1436 context{state: stateText}, 1437 }, 1438 { 1439 `<script>foo</script><!--`, 1440 context{state: stateHTMLCmt}, 1441 }, 1442 { 1443 `<script>document.write("<p>foo</p>");`, 1444 context{state: stateJS, element: elementScript}, 1445 }, 1446 { 1447 `<script>document.write("<p>foo<\/script>");`, 1448 context{state: stateJS, element: elementScript}, 1449 }, 1450 { 1451 `<script>document.write("<script>alert(1)</script>");`, 1452 context{state: stateText}, 1453 }, 1454 { 1455 `<script type="text/template">`, 1456 context{state: stateText}, 1457 }, 1458 // covering issue 19968 1459 { 1460 `<script type="TEXT/JAVASCRIPT">`, 1461 context{state: stateJS, element: elementScript}, 1462 }, 1463 // covering issue 19965 1464 { 1465 `<script TYPE="text/template">`, 1466 context{state: stateText}, 1467 }, 1468 { 1469 `<script type="notjs">`, 1470 context{state: stateText}, 1471 }, 1472 { 1473 `<Script>`, 1474 context{state: stateJS, element: elementScript}, 1475 }, 1476 { 1477 `<SCRIPT>foo`, 1478 context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, 1479 }, 1480 { 1481 `<textarea>value`, 1482 context{state: stateRCDATA, element: elementTextarea}, 1483 }, 1484 { 1485 `<textarea>value</TEXTAREA>`, 1486 context{state: stateText}, 1487 }, 1488 { 1489 `<textarea name=html><b`, 1490 context{state: stateRCDATA, element: elementTextarea}, 1491 }, 1492 { 1493 `<title>value`, 1494 context{state: stateRCDATA, element: elementTitle}, 1495 }, 1496 { 1497 `<style>value`, 1498 context{state: stateCSS, element: elementStyle}, 1499 }, 1500 { 1501 `<a xlink:href`, 1502 context{state: stateAttrName, attr: attrURL}, 1503 }, 1504 { 1505 `<a xmlns`, 1506 context{state: stateAttrName, attr: attrURL}, 1507 }, 1508 { 1509 `<a xmlns:foo`, 1510 context{state: stateAttrName, attr: attrURL}, 1511 }, 1512 { 1513 `<a xmlnsxyz`, 1514 context{state: stateAttrName}, 1515 }, 1516 { 1517 `<a data-url`, 1518 context{state: stateAttrName, attr: attrURL}, 1519 }, 1520 { 1521 `<a data-iconUri`, 1522 context{state: stateAttrName, attr: attrURL}, 1523 }, 1524 { 1525 `<a data-urlItem`, 1526 context{state: stateAttrName, attr: attrURL}, 1527 }, 1528 { 1529 `<a g:`, 1530 context{state: stateAttrName}, 1531 }, 1532 { 1533 `<a g:url`, 1534 context{state: stateAttrName, attr: attrURL}, 1535 }, 1536 { 1537 `<a g:iconUri`, 1538 context{state: stateAttrName, attr: attrURL}, 1539 }, 1540 { 1541 `<a g:urlItem`, 1542 context{state: stateAttrName, attr: attrURL}, 1543 }, 1544 { 1545 `<a g:value`, 1546 context{state: stateAttrName}, 1547 }, 1548 { 1549 `<a svg:style='`, 1550 context{state: stateCSS, delim: delimSingleQuote, attr: attrStyle}, 1551 }, 1552 { 1553 `<svg:font-face`, 1554 context{state: stateTag}, 1555 }, 1556 { 1557 `<svg:a svg:onclick="`, 1558 context{state: stateJS, delim: delimDoubleQuote, attr: attrScript}, 1559 }, 1560 { 1561 `<svg:a svg:onclick="x()">`, 1562 context{}, 1563 }, 1564 } 1565 1566 for _, test := range tests { 1567 b, e := []byte(test.input), makeEscaper(nil) 1568 c := e.escapeText(context{}, &parse.TextNode{NodeType: parse.NodeText, Text: b}) 1569 if !test.output.eq(c) { 1570 t.Errorf("input %q: want context\n\t%v\ngot\n\t%v", test.input, test.output, c) 1571 continue 1572 } 1573 if test.input != string(b) { 1574 t.Errorf("input %q: text node was modified: want %q got %q", test.input, test.input, b) 1575 continue 1576 } 1577 } 1578 } 1579 1580 func TestEnsurePipelineContains(t *testing.T) { 1581 tests := []struct { 1582 input, output string 1583 ids []string 1584 }{ 1585 { 1586 "{{.X}}", 1587 ".X", 1588 []string{}, 1589 }, 1590 { 1591 "{{.X | html}}", 1592 ".X | html", 1593 []string{}, 1594 }, 1595 { 1596 "{{.X}}", 1597 ".X | html", 1598 []string{"html"}, 1599 }, 1600 { 1601 "{{html .X}}", 1602 "_eval_args_ .X | html | urlquery", 1603 []string{"html", "urlquery"}, 1604 }, 1605 { 1606 "{{html .X .Y .Z}}", 1607 "_eval_args_ .X .Y .Z | html | urlquery", 1608 []string{"html", "urlquery"}, 1609 }, 1610 { 1611 "{{.X | print}}", 1612 ".X | print | urlquery", 1613 []string{"urlquery"}, 1614 }, 1615 { 1616 "{{.X | print | urlquery}}", 1617 ".X | print | urlquery", 1618 []string{"urlquery"}, 1619 }, 1620 { 1621 "{{.X | urlquery}}", 1622 ".X | html | urlquery", 1623 []string{"html", "urlquery"}, 1624 }, 1625 { 1626 "{{.X | print 2 | .f 3}}", 1627 ".X | print 2 | .f 3 | urlquery | html", 1628 []string{"urlquery", "html"}, 1629 }, 1630 { 1631 // covering issue 10801 1632 "{{.X | println.x }}", 1633 ".X | println.x | urlquery | html", 1634 []string{"urlquery", "html"}, 1635 }, 1636 { 1637 // covering issue 10801 1638 "{{.X | (print 12 | println).x }}", 1639 ".X | (print 12 | println).x | urlquery | html", 1640 []string{"urlquery", "html"}, 1641 }, 1642 // The following test cases ensure that the merging of internal escapers 1643 // with the predefined "html" and "urlquery" escapers is correct. 1644 { 1645 "{{.X | urlquery}}", 1646 ".X | _html_template_urlfilter | urlquery", 1647 []string{"_html_template_urlfilter", "_html_template_urlnormalizer"}, 1648 }, 1649 { 1650 "{{.X | urlquery}}", 1651 ".X | urlquery | _html_template_urlfilter | _html_template_cssescaper", 1652 []string{"_html_template_urlfilter", "_html_template_cssescaper"}, 1653 }, 1654 { 1655 "{{.X | urlquery}}", 1656 ".X | urlquery", 1657 []string{"_html_template_urlnormalizer"}, 1658 }, 1659 { 1660 "{{.X | urlquery}}", 1661 ".X | urlquery", 1662 []string{"_html_template_urlescaper"}, 1663 }, 1664 { 1665 "{{.X | html}}", 1666 ".X | html", 1667 []string{"_html_template_htmlescaper"}, 1668 }, 1669 { 1670 "{{.X | html}}", 1671 ".X | html", 1672 []string{"_html_template_rcdataescaper"}, 1673 }, 1674 } 1675 for i, test := range tests { 1676 tmpl := template.Must(template.New("test").Parse(test.input)) 1677 action, ok := (tmpl.Tree.Root.Nodes[0].(*parse.ActionNode)) 1678 if !ok { 1679 t.Errorf("First node is not an action: %s", test.input) 1680 continue 1681 } 1682 pipe := action.Pipe 1683 originalIDs := make([]string, len(test.ids)) 1684 copy(originalIDs, test.ids) 1685 ensurePipelineContains(pipe, test.ids) 1686 got := pipe.String() 1687 if got != test.output { 1688 t.Errorf("#%d: %s, %v: want\n\t%s\ngot\n\t%s", i, test.input, originalIDs, test.output, got) 1689 } 1690 } 1691 } 1692 1693 func TestEscapeMalformedPipelines(t *testing.T) { 1694 tests := []string{ 1695 "{{ 0 | $ }}", 1696 "{{ 0 | $ | urlquery }}", 1697 "{{ 0 | (nil) }}", 1698 "{{ 0 | (nil) | html }}", 1699 } 1700 for _, test := range tests { 1701 var b bytes.Buffer 1702 tmpl, err := New("test").Parse(test) 1703 if err != nil { 1704 t.Errorf("failed to parse set: %q", err) 1705 } 1706 err = tmpl.Execute(&b, nil) 1707 if err == nil { 1708 t.Errorf("Expected error for %q", test) 1709 } 1710 } 1711 } 1712 1713 func TestEscapeErrorsNotIgnorable(t *testing.T) { 1714 var b bytes.Buffer 1715 tmpl, _ := New("dangerous").Parse("<a") 1716 err := tmpl.Execute(&b, nil) 1717 if err == nil { 1718 t.Errorf("Expected error") 1719 } else if b.Len() != 0 { 1720 t.Errorf("Emitted output despite escaping failure") 1721 } 1722 } 1723 1724 func TestEscapeSetErrorsNotIgnorable(t *testing.T) { 1725 var b bytes.Buffer 1726 tmpl, err := New("root").Parse(`{{define "t"}}<a{{end}}`) 1727 if err != nil { 1728 t.Errorf("failed to parse set: %q", err) 1729 } 1730 err = tmpl.ExecuteTemplate(&b, "t", nil) 1731 if err == nil { 1732 t.Errorf("Expected error") 1733 } else if b.Len() != 0 { 1734 t.Errorf("Emitted output despite escaping failure") 1735 } 1736 } 1737 1738 func TestRedundantFuncs(t *testing.T) { 1739 inputs := []interface{}{ 1740 "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + 1741 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + 1742 ` !"#$%&'()*+,-./` + 1743 `0123456789:;<=>?` + 1744 `@ABCDEFGHIJKLMNO` + 1745 `PQRSTUVWXYZ[\]^_` + 1746 "`abcdefghijklmno" + 1747 "pqrstuvwxyz{|}~\x7f" + 1748 "\u00A0\u0100\u2028\u2029\ufeff\ufdec\ufffd\uffff\U0001D11E" + 1749 "&%22\\", 1750 CSS(`a[href =~ "//example.com"]#foo`), 1751 HTML(`Hello, <b>World</b> &tc!`), 1752 HTMLAttr(` dir="ltr"`), 1753 JS(`c && alert("Hello, World!");`), 1754 JSStr(`Hello, World & O'Reilly\x21`), 1755 URL(`greeting=H%69&addressee=(World)`), 1756 } 1757 1758 for n0, m := range redundantFuncs { 1759 f0 := funcMap[n0].(func(...interface{}) string) 1760 for n1 := range m { 1761 f1 := funcMap[n1].(func(...interface{}) string) 1762 for _, input := range inputs { 1763 want := f0(input) 1764 if got := f1(want); want != got { 1765 t.Errorf("%s %s with %T %q: want\n\t%q,\ngot\n\t%q", n0, n1, input, input, want, got) 1766 } 1767 } 1768 } 1769 } 1770 } 1771 1772 func TestIndirectPrint(t *testing.T) { 1773 a := 3 1774 ap := &a 1775 b := "hello" 1776 bp := &b 1777 bpp := &bp 1778 tmpl := Must(New("t").Parse(`{{.}}`)) 1779 var buf bytes.Buffer 1780 err := tmpl.Execute(&buf, ap) 1781 if err != nil { 1782 t.Errorf("Unexpected error: %s", err) 1783 } else if buf.String() != "3" { 1784 t.Errorf(`Expected "3"; got %q`, buf.String()) 1785 } 1786 buf.Reset() 1787 err = tmpl.Execute(&buf, bpp) 1788 if err != nil { 1789 t.Errorf("Unexpected error: %s", err) 1790 } else if buf.String() != "hello" { 1791 t.Errorf(`Expected "hello"; got %q`, buf.String()) 1792 } 1793 } 1794 1795 // This is a test for issue 3272. 1796 func TestEmptyTemplate(t *testing.T) { 1797 page := Must(New("page").ParseFiles(os.DevNull)) 1798 if err := page.ExecuteTemplate(os.Stdout, "page", "nothing"); err == nil { 1799 t.Fatal("expected error") 1800 } 1801 } 1802 1803 type Issue7379 int 1804 1805 func (Issue7379) SomeMethod(x int) string { 1806 return fmt.Sprintf("<%d>", x) 1807 } 1808 1809 // This is a test for issue 7379: type assertion error caused panic, and then 1810 // the code to handle the panic breaks escaping. It's hard to see the second 1811 // problem once the first is fixed, but its fix is trivial so we let that go. See 1812 // the discussion for issue 7379. 1813 func TestPipeToMethodIsEscaped(t *testing.T) { 1814 tmpl := Must(New("x").Parse("<html>{{0 | .SomeMethod}}</html>\n")) 1815 tryExec := func() string { 1816 defer func() { 1817 panicValue := recover() 1818 if panicValue != nil { 1819 t.Errorf("panicked: %v\n", panicValue) 1820 } 1821 }() 1822 var b bytes.Buffer 1823 tmpl.Execute(&b, Issue7379(0)) 1824 return b.String() 1825 } 1826 for i := 0; i < 3; i++ { 1827 str := tryExec() 1828 const expect = "<html><0></html>\n" 1829 if str != expect { 1830 t.Errorf("expected %q got %q", expect, str) 1831 } 1832 } 1833 } 1834 1835 // Unlike text/template, html/template crashed if given an incomplete 1836 // template, that is, a template that had been named but not given any content. 1837 // This is issue #10204. 1838 func TestErrorOnUndefined(t *testing.T) { 1839 tmpl := New("undefined") 1840 1841 err := tmpl.Execute(nil, nil) 1842 if err == nil { 1843 t.Fatal("expected error") 1844 } 1845 if !strings.Contains(err.Error(), "incomplete") { 1846 t.Errorf("expected error about incomplete template; got %s", err) 1847 } 1848 } 1849 1850 // This covers issue #20842. 1851 func TestIdempotentExecute(t *testing.T) { 1852 tmpl := Must(New(""). 1853 Parse(`{{define "main"}}<body>{{template "hello"}}</body>{{end}}`)) 1854 Must(tmpl. 1855 Parse(`{{define "hello"}}Hello, {{"Ladies & Gentlemen!"}}{{end}}`)) 1856 got := new(bytes.Buffer) 1857 var err error 1858 // Ensure that "hello" produces the same output when executed twice. 1859 want := "Hello, Ladies & Gentlemen!" 1860 for i := 0; i < 2; i++ { 1861 err = tmpl.ExecuteTemplate(got, "hello", nil) 1862 if err != nil { 1863 t.Fatalf("unexpected error: %s", err) 1864 } 1865 if got.String() != want { 1866 t.Fatalf("after executing template \"hello\", got:\n\t%q\nwant:\n\t%q\n", got.String(), want) 1867 } 1868 got.Reset() 1869 } 1870 // Ensure that the implicit re-execution of "hello" during the execution of 1871 // "main" does not cause the output of "hello" to change. 1872 err = tmpl.ExecuteTemplate(got, "main", nil) 1873 if err != nil { 1874 t.Fatalf("unexpected error: %s", err) 1875 } 1876 // If the HTML escaper is added again to the action {{"Ladies & Gentlemen!"}}, 1877 // we would expected to see the ampersand overescaped to "&amp;". 1878 want = "<body>Hello, Ladies & Gentlemen!</body>" 1879 if got.String() != want { 1880 t.Errorf("after executing template \"main\", got:\n\t%q\nwant:\n\t%q\n", got.String(), want) 1881 } 1882 } 1883 1884 // This covers issue #21844. 1885 func TestAddExistingTreeError(t *testing.T) { 1886 tmpl := Must(New("foo").Parse(`<p>{{.}}</p>`)) 1887 tmpl, err := tmpl.AddParseTree("bar", tmpl.Tree) 1888 if err == nil { 1889 t.Fatalf("expected error after AddParseTree") 1890 } 1891 const want = `html/template: cannot add parse tree that template "foo" already references` 1892 if got := err.Error(); got != want { 1893 t.Errorf("got error:\n\t%q\nwant:\n\t%q\n", got, want) 1894 } 1895 } 1896 1897 func BenchmarkEscapedExecute(b *testing.B) { 1898 tmpl := Must(New("t").Parse(`<a onclick="alert('{{.}}')">{{.}}</a>`)) 1899 var buf bytes.Buffer 1900 b.ResetTimer() 1901 for i := 0; i < b.N; i++ { 1902 tmpl.Execute(&buf, "foo & 'bar' & baz") 1903 buf.Reset() 1904 } 1905 }