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