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