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 5package template 6 7import ( 8 "bytes" 9 "encoding/json" 10 "fmt" 11 "os" 12 "strings" 13 "testing" 14 "text/template" 15 "text/template/parse" 16) 17 18type badMarshaler struct{} 19 20func (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 25type goodMarshaler struct{} 26 27func (x *goodMarshaler) MarshalJSON() ([]byte, error) { 28 return []byte(`{ "<foo>": "O'Reilly" }`), nil 29} 30 31func TestEscape(t *testing.T) { 32 data := struct { 33 F, T bool 34 C, G, H, I string 35 A, E []string 36 B, M json.Marshaler 37 N int 38 U any // untyped nil 39 Z *int // typed nil 40 W HTML 41 }{ 42 F: false, 43 T: true, 44 C: "<Cincinnati>", 45 G: "<Goodbye>", 46 H: "<Hello>", 47 A: []string{"<a>", "<b>"}, 48 E: []string{}, 49 N: 42, 50 B: &badMarshaler{}, 51 M: &goodMarshaler{}, 52 U: nil, 53 Z: nil, 54 W: HTML(`¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`), 55 I: "${ asd `` }", 56 } 57 pdata := &data 58 59 tests := []struct { 60 name string 61 input string 62 output string 63 }{ 64 { 65 "if", 66 "{{if .T}}Hello{{end}}, {{.C}}!", 67 "Hello, <Cincinnati>!", 68 }, 69 { 70 "else", 71 "{{if .F}}{{.H}}{{else}}{{.G}}{{end}}!", 72 "<Goodbye>!", 73 }, 74 { 75 "overescaping1", 76 "Hello, {{.C | html}}!", 77 "Hello, <Cincinnati>!", 78 }, 79 { 80 "overescaping2", 81 "Hello, {{html .C}}!", 82 "Hello, <Cincinnati>!", 83 }, 84 { 85 "overescaping3", 86 "{{with .C}}{{$msg := .}}Hello, {{$msg}}!{{end}}", 87 "Hello, <Cincinnati>!", 88 }, 89 { 90 "assignment", 91 "{{if $x := .H}}{{$x}}{{end}}", 92 "<Hello>", 93 }, 94 { 95 "withBody", 96 "{{with .H}}{{.}}{{end}}", 97 "<Hello>", 98 }, 99 { 100 "withElse", 101 "{{with .E}}{{.}}{{else}}{{.H}}{{end}}", 102 "<Hello>", 103 }, 104 { 105 "rangeBody", 106 "{{range .A}}{{.}}{{end}}", 107 "<a><b>", 108 }, 109 { 110 "rangeElse", 111 "{{range .E}}{{.}}{{else}}{{.H}}{{end}}", 112 "<Hello>", 113 }, 114 { 115 "nonStringValue", 116 "{{.T}}", 117 "true", 118 }, 119 { 120 "untypedNilValue", 121 "{{.U}}", 122 "", 123 }, 124 { 125 "typedNilValue", 126 "{{.Z}}", 127 "<nil>", 128 }, 129 { 130 "constant", 131 `<a href="/search?q={{"'a<b'"}}">`, 132 `<a href="/search?q=%27a%3cb%27">`, 133 }, 134 { 135 "multipleAttrs", 136 "<a b=1 c={{.H}}>", 137 "<a b=1 c=<Hello>>", 138 }, 139 { 140 "urlStartRel", 141 `<a href='{{"/foo/bar?a=b&c=d"}}'>`, 142 `<a href='/foo/bar?a=b&c=d'>`, 143 }, 144 { 145 "urlStartAbsOk", 146 `<a href='{{"http://example.com/foo/bar?a=b&c=d"}}'>`, 147 `<a href='http://example.com/foo/bar?a=b&c=d'>`, 148 }, 149 { 150 "protocolRelativeURLStart", 151 `<a href='{{"//example.com:8000/foo/bar?a=b&c=d"}}'>`, 152 `<a href='//example.com:8000/foo/bar?a=b&c=d'>`, 153 }, 154 { 155 "pathRelativeURLStart", 156 `<a href="{{"/javascript:80/foo/bar"}}">`, 157 `<a href="/javascript:80/foo/bar">`, 158 }, 159 { 160 "dangerousURLStart", 161 `<a href='{{"javascript:alert(%22pwned%22)"}}'>`, 162 `<a href='#ZgotmplZ'>`, 163 }, 164 { 165 "dangerousURLStart2", 166 `<a href=' {{"javascript:alert(%22pwned%22)"}}'>`, 167 `<a href=' #ZgotmplZ'>`, 168 }, 169 { 170 "nonHierURL", 171 `<a href={{"mailto:Muhammed \"The Greatest\" Ali <[email protected]>"}}>`, 172 `<a href=mailto:Muhammed%20%22The%20Greatest%22%20Ali%20%3cm.ali@example.com%3e>`, 173 }, 174 { 175 "urlPath", 176 `<a href='http://{{"javascript:80"}}/foo'>`, 177 `<a href='http://javascript:80/foo'>`, 178 }, 179 { 180 "urlQuery", 181 `<a href='/search?q={{.H}}'>`, 182 `<a href='/search?q=%3cHello%3e'>`, 183 }, 184 { 185 "urlFragment", 186 `<a href='/faq#{{.H}}'>`, 187 `<a href='/faq#%3cHello%3e'>`, 188 }, 189 { 190 "urlBranch", 191 `<a href="{{if .F}}/foo?a=b{{else}}/bar{{end}}">`, 192 `<a href="/bar">`, 193 }, 194 { 195 "urlBranchConflictMoot", 196 `<a href="{{if .T}}/foo?a={{else}}/bar#{{end}}{{.C}}">`, 197 `<a href="/foo?a=%3cCincinnati%3e">`, 198 }, 199 { 200 "jsStrValue", 201 "<button onclick='alert({{.H}})'>", 202 `<button onclick='alert("\u003cHello\u003e")'>`, 203 }, 204 { 205 "jsNumericValue", 206 "<button onclick='alert({{.N}})'>", 207 `<button onclick='alert( 42 )'>`, 208 }, 209 { 210 "jsBoolValue", 211 "<button onclick='alert({{.T}})'>", 212 `<button onclick='alert( true )'>`, 213 }, 214 { 215 "jsNilValueTyped", 216 "<button onclick='alert(typeof{{.Z}})'>", 217 `<button onclick='alert(typeof null )'>`, 218 }, 219 { 220 "jsNilValueUntyped", 221 "<button onclick='alert(typeof{{.U}})'>", 222 `<button onclick='alert(typeof null )'>`, 223 }, 224 { 225 "jsObjValue", 226 "<button onclick='alert({{.A}})'>", 227 `<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`, 228 }, 229 { 230 "jsObjValueScript", 231 "<script>alert({{.A}})</script>", 232 `<script>alert(["\u003ca\u003e","\u003cb\u003e"])</script>`, 233 }, 234 { 235 "jsObjValueNotOverEscaped", 236 "<button onclick='alert({{.A | html}})'>", 237 `<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`, 238 }, 239 { 240 "jsStr", 241 "<button onclick='alert("{{.H}}")'>", 242 `<button onclick='alert("\u003cHello\u003e")'>`, 243 }, 244 { 245 "badMarshaler", 246 `<button onclick='alert(1/{{.B}}in numbers)'>`, 247 `<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)'>`, 248 }, 249 { 250 "jsMarshaler", 251 `<button onclick='alert({{.M}})'>`, 252 `<button onclick='alert({"\u003cfoo\u003e":"O'Reilly"})'>`, 253 }, 254 { 255 "jsStrNotUnderEscaped", 256 "<button onclick='alert({{.C | urlquery}})'>", 257 // URL escaped, then quoted for JS. 258 `<button onclick='alert("%3CCincinnati%3E")'>`, 259 }, 260 { 261 "jsRe", 262 `<button onclick='alert(/{{"foo+bar"}}/.test(""))'>`, 263 `<button onclick='alert(/foo\u002bbar/.test(""))'>`, 264 }, 265 { 266 "jsReBlank", 267 `<script>alert(/{{""}}/.test(""));</script>`, 268 `<script>alert(/(?:)/.test(""));</script>`, 269 }, 270 { 271 "jsReAmbigOk", 272 `<script>{{if true}}var x = 1{{end}}</script>`, 273 // The {if} ends in an ambiguous jsCtx but there is 274 // no slash following so we shouldn't care. 275 `<script>var x = 1</script>`, 276 }, 277 { 278 "styleBidiKeywordPassed", 279 `<p style="dir: {{"ltr"}}">`, 280 `<p style="dir: ltr">`, 281 }, 282 { 283 "styleBidiPropNamePassed", 284 `<p style="border-{{"left"}}: 0; border-{{"right"}}: 1in">`, 285 `<p style="border-left: 0; border-right: 1in">`, 286 }, 287 { 288 "styleExpressionBlocked", 289 `<p style="width: {{"expression(alert(1337))"}}">`, 290 `<p style="width: ZgotmplZ">`, 291 }, 292 { 293 "styleTagSelectorPassed", 294 `<style>{{"p"}} { color: pink }</style>`, 295 `<style>p { color: pink }</style>`, 296 }, 297 { 298 "styleIDPassed", 299 `<style>p{{"#my-ID"}} { font: Arial }</style>`, 300 `<style>p#my-ID { font: Arial }</style>`, 301 }, 302 { 303 "styleClassPassed", 304 `<style>p{{".my_class"}} { font: Arial }</style>`, 305 `<style>p.my_class { font: Arial }</style>`, 306 }, 307 { 308 "styleQuantityPassed", 309 `<a style="left: {{"2em"}}; top: {{0}}">`, 310 `<a style="left: 2em; top: 0">`, 311 }, 312 { 313 "stylePctPassed", 314 `<table style=width:{{"100%"}}>`, 315 `<table style=width:100%>`, 316 }, 317 { 318 "styleColorPassed", 319 `<p style="color: {{"#8ff"}}; background: {{"#000"}}">`, 320 `<p style="color: #8ff; background: #000">`, 321 }, 322 { 323 "styleObfuscatedExpressionBlocked", 324 `<p style="width: {{" e\\78preS\x00Sio/**/n(alert(1337))"}}">`, 325 `<p style="width: ZgotmplZ">`, 326 }, 327 { 328 "styleMozBindingBlocked", 329 `<p style="{{"-moz-binding(alert(1337))"}}: ...">`, 330 `<p style="ZgotmplZ: ...">`, 331 }, 332 { 333 "styleObfuscatedMozBindingBlocked", 334 `<p style="{{" -mo\\7a-B\x00I/**/nding(alert(1337))"}}: ...">`, 335 `<p style="ZgotmplZ: ...">`, 336 }, 337 { 338 "styleFontNameString", 339 `<p style='font-family: "{{"Times New Roman"}}"'>`, 340 `<p style='font-family: "Times New Roman"'>`, 341 }, 342 { 343 "styleFontNameString", 344 `<p style='font-family: "{{"Times New Roman"}}", "{{"sans-serif"}}"'>`, 345 `<p style='font-family: "Times New Roman", "sans-serif"'>`, 346 }, 347 { 348 "styleFontNameUnquoted", 349 `<p style='font-family: {{"Times New Roman"}}'>`, 350 `<p style='font-family: Times New Roman'>`, 351 }, 352 { 353 "styleURLQueryEncoded", 354 `<p style="background: url(/img?name={{"O'Reilly Animal(1)<2>.png"}})">`, 355 `<p style="background: url(/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png)">`, 356 }, 357 { 358 "styleQuotedURLQueryEncoded", 359 `<p style="background: url('/img?name={{"O'Reilly Animal(1)<2>.png"}}')">`, 360 `<p style="background: url('/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png')">`, 361 }, 362 { 363 "styleStrQueryEncoded", 364 `<p style="background: '/img?name={{"O'Reilly Animal(1)<2>.png"}}'">`, 365 `<p style="background: '/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png'">`, 366 }, 367 { 368 "styleURLBadProtocolBlocked", 369 `<a style="background: url('{{"javascript:alert(1337)"}}')">`, 370 `<a style="background: url('#ZgotmplZ')">`, 371 }, 372 { 373 "styleStrBadProtocolBlocked", 374 `<a style="background: '{{"vbscript:alert(1337)"}}'">`, 375 `<a style="background: '#ZgotmplZ'">`, 376 }, 377 { 378 "styleStrEncodedProtocolEncoded", 379 `<a style="background: '{{"javascript\\3a alert(1337)"}}'">`, 380 // The CSS string 'javascript\\3a alert(1337)' does not contain a colon. 381 `<a style="background: 'javascript\\3a alert\28 1337\29 '">`, 382 }, 383 { 384 "styleURLGoodProtocolPassed", 385 `<a style="background: url('{{"http://oreilly.com/O'Reilly Animals(1)<2>;{}.html"}}')">`, 386 `<a style="background: url('http://oreilly.com/O%27Reilly%20Animals%281%29%3c2%3e;%7b%7d.html')">`, 387 }, 388 { 389 "styleStrGoodProtocolPassed", 390 `<a style="background: '{{"http://oreilly.com/O'Reilly Animals(1)<2>;{}.html"}}'">`, 391 `<a style="background: 'http\3a\2f\2foreilly.com\2fO\27Reilly Animals\28 1\29\3c 2\3e\3b\7b\7d.html'">`, 392 }, 393 { 394 "styleURLEncodedForHTMLInAttr", 395 `<a style="background: url('{{"/search?img=foo&size=icon"}}')">`, 396 `<a style="background: url('/search?img=foo&size=icon')">`, 397 }, 398 { 399 "styleURLNotEncodedForHTMLInCdata", 400 `<style>body { background: url('{{"/search?img=foo&size=icon"}}') }</style>`, 401 `<style>body { background: url('/search?img=foo&size=icon') }</style>`, 402 }, 403 { 404 "styleURLMixedCase", 405 `<p style="background: URL(#{{.H}})">`, 406 `<p style="background: URL(#%3cHello%3e)">`, 407 }, 408 { 409 "stylePropertyPairPassed", 410 `<a style='{{"color: red"}}'>`, 411 `<a style='color: red'>`, 412 }, 413 { 414 "styleStrSpecialsEncoded", 415 `<a style="font-family: '{{"/**/'\";:// \\"}}', "{{"/**/'\";:// \\"}}"">`, 416 `<a style="font-family: '\2f**\2f\27\22\3b\3a\2f\2f \\', "\2f**\2f\27\22\3b\3a\2f\2f \\"">`, 417 }, 418 { 419 "styleURLSpecialsEncoded", 420 `<a style="border-image: url({{"/**/'\";:// \\"}}), url("{{"/**/'\";:// \\"}}"), url('{{"/**/'\";:// \\"}}'), 'http://www.example.com/?q={{"/**/'\";:// \\"}}''">`, 421 `<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''">`, 422 }, 423 { 424 "HTML comment", 425 "<b>Hello, <!-- name of world -->{{.C}}</b>", 426 "<b>Hello, <Cincinnati></b>", 427 }, 428 { 429 "HTML comment not first < in text node.", 430 "<<!-- -->!--", 431 "<!--", 432 }, 433 { 434 "HTML normalization 1", 435 "a < b", 436 "a < b", 437 }, 438 { 439 "HTML normalization 2", 440 "a << b", 441 "a << b", 442 }, 443 { 444 "HTML normalization 3", 445 "a<<!-- --><!-- -->b", 446 "a<b", 447 }, 448 { 449 "HTML doctype not normalized", 450 "<!DOCTYPE html>Hello, World!", 451 "<!DOCTYPE html>Hello, World!", 452 }, 453 { 454 "HTML doctype not case-insensitive", 455 "<!doCtYPE htMl>Hello, World!", 456 "<!doCtYPE htMl>Hello, World!", 457 }, 458 { 459 "No doctype injection", 460 `<!{{"DOCTYPE"}}`, 461 "<!DOCTYPE", 462 }, 463 { 464 "Split HTML comment", 465 "<b>Hello, <!-- name of {{if .T}}city -->{{.C}}{{else}}world -->{{.W}}{{end}}</b>", 466 "<b>Hello, <Cincinnati></b>", 467 }, 468 { 469 "JS line comment", 470 "<script>for (;;) { if (c()) break// foo not a label\n" + 471 "foo({{.T}});}</script>", 472 "<script>for (;;) { if (c()) break\n" + 473 "foo( true );}</script>", 474 }, 475 { 476 "JS multiline block comment", 477 "<script>for (;;) { if (c()) break/* foo not a label\n" + 478 " */foo({{.T}});}</script>", 479 // Newline separates break from call. If newline 480 // removed, then break will consume label leaving 481 // code invalid. 482 "<script>for (;;) { if (c()) break\n" + 483 "foo( true );}</script>", 484 }, 485 { 486 "JS single-line block comment", 487 "<script>for (;;) {\n" + 488 "if (c()) break/* foo a label */foo;" + 489 "x({{.T}});}</script>", 490 // Newline separates break from call. If newline 491 // removed, then break will consume label leaving 492 // code invalid. 493 "<script>for (;;) {\n" + 494 "if (c()) break foo;" + 495 "x( true );}</script>", 496 }, 497 { 498 "JS block comment flush with mathematical division", 499 "<script>var a/*b*//c\nd</script>", 500 "<script>var a /c\nd</script>", 501 }, 502 { 503 "JS mixed comments", 504 "<script>var a/*b*///c\nd</script>", 505 "<script>var a \nd</script>", 506 }, 507 { 508 "JS HTML-like comments", 509 "<script>before <!-- beep\nbetween\nbefore-->boop\n</script>", 510 "<script>before \nbetween\nbefore\n</script>", 511 }, 512 { 513 "JS hashbang comment", 514 "<script>#! beep\n</script>", 515 "<script>\n</script>", 516 }, 517 { 518 "Special tags in <script> string literals", 519 `<script>var a = "asd < 123 <!-- 456 < fgh <script jkl < 789 </script"</script>`, 520 `<script>var a = "asd < 123 \x3C!-- 456 < fgh \x3Cscript jkl < 789 \x3C/script"</script>`, 521 }, 522 { 523 "Special tags in <script> string literals (mixed case)", 524 `<script>var a = "<!-- <ScripT </ScripT"</script>`, 525 `<script>var a = "\x3C!-- \x3CScripT \x3C/ScripT"</script>`, 526 }, 527 { 528 "Special tags in <script> regex literals (mixed case)", 529 `<script>var a = /<!-- <ScripT </ScripT/</script>`, 530 `<script>var a = /\x3C!-- \x3CScripT \x3C/ScripT/</script>`, 531 }, 532 { 533 "CSS comments", 534 "<style>p// paragraph\n" + 535 `{border: 1px/* color */{{"#00f"}}}</style>`, 536 "<style>p\n" + 537 "{border: 1px #00f}</style>", 538 }, 539 { 540 "JS attr block comment", 541 `<a onclick="f(""); /* alert({{.H}}) */">`, 542 // Attribute comment tests should pass if the comments 543 // are successfully elided. 544 `<a onclick="f(""); /* alert() */">`, 545 }, 546 { 547 "JS attr line comment", 548 `<a onclick="// alert({{.G}})">`, 549 `<a onclick="// alert()">`, 550 }, 551 { 552 "CSS attr block comment", 553 `<a style="/* color: {{.H}} */">`, 554 `<a style="/* color: */">`, 555 }, 556 { 557 "CSS attr line comment", 558 `<a style="// color: {{.G}}">`, 559 `<a style="// color: ">`, 560 }, 561 { 562 "HTML substitution commented out", 563 "<p><!-- {{.H}} --></p>", 564 "<p></p>", 565 }, 566 { 567 "Comment ends flush with start", 568 "<!--{{.}}--><script>/*{{.}}*///{{.}}\n</script><style>/*{{.}}*///{{.}}\n</style><a onclick='/*{{.}}*///{{.}}' style='/*{{.}}*///{{.}}'>", 569 "<script> \n</script><style> \n</style><a onclick='/**///' style='/**///'>", 570 }, 571 { 572 "typed HTML in text", 573 `{{.W}}`, 574 `¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`, 575 }, 576 { 577 "typed HTML in attribute", 578 `<div title="{{.W}}">`, 579 `<div title="¡Hello, O'World!">`, 580 }, 581 { 582 "typed HTML in script", 583 `<button onclick="alert({{.W}})">`, 584 `<button onclick="alert("\u0026iexcl;\u003cb class=\"foo\"\u003eHello\u003c/b\u003e, \u003ctextarea\u003eO'World\u003c/textarea\u003e!")">`, 585 }, 586 { 587 "typed HTML in RCDATA", 588 `<textarea>{{.W}}</textarea>`, 589 `<textarea>¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!</textarea>`, 590 }, 591 { 592 "range in textarea", 593 "<textarea>{{range .A}}{{.}}{{end}}</textarea>", 594 "<textarea><a><b></textarea>", 595 }, 596 { 597 "No tag injection", 598 `{{"10$"}}<{{"script src,evil.org/pwnd.js"}}...`, 599 `10$<script src,evil.org/pwnd.js...`, 600 }, 601 { 602 "No comment injection", 603 `<{{"!--"}}`, 604 `<!--`, 605 }, 606 { 607 "No RCDATA end tag injection", 608 `<textarea><{{"/textarea "}}...</textarea>`, 609 `<textarea></textarea ...</textarea>`, 610 }, 611 { 612 "optional attrs", 613 `<img class="{{"iconClass"}}"` + 614 `{{if .T}} id="{{"<iconId>"}}"{{end}}` + 615 // Double quotes inside if/else. 616 ` src=` + 617 `{{if .T}}"?{{"<iconPath>"}}"` + 618 `{{else}}"images/cleardot.gif"{{end}}` + 619 // Missing space before title, but it is not a 620 // part of the src attribute. 621 `{{if .T}}title="{{"<title>"}}"{{end}}` + 622 // Quotes outside if/else. 623 ` alt="` + 624 `{{if .T}}{{"<alt>"}}` + 625 `{{else}}{{if .F}}{{"<title>"}}{{end}}` + 626 `{{end}}"` + 627 `>`, 628 `<img class="iconClass" id="<iconId>" src="?%3ciconPath%3e"title="<title>" alt="<alt>">`, 629 }, 630 { 631 "conditional valueless attr name", 632 `<input{{if .T}} checked{{end}} name=n>`, 633 `<input checked name=n>`, 634 }, 635 { 636 "conditional dynamic valueless attr name 1", 637 `<input{{if .T}} {{"checked"}}{{end}} name=n>`, 638 `<input checked name=n>`, 639 }, 640 { 641 "conditional dynamic valueless attr name 2", 642 `<input {{if .T}}{{"checked"}} {{end}}name=n>`, 643 `<input checked name=n>`, 644 }, 645 { 646 "dynamic attribute name", 647 `<img on{{"load"}}="alert({{"loaded"}})">`, 648 // Treated as JS since quotes are inserted. 649 `<img onload="alert("loaded")">`, 650 }, 651 { 652 "bad dynamic attribute name 1", 653 // Allow checked, selected, disabled, but not JS or 654 // CSS attributes. 655 `<input {{"onchange"}}="{{"doEvil()"}}">`, 656 `<input ZgotmplZ="doEvil()">`, 657 }, 658 { 659 "bad dynamic attribute name 2", 660 `<div {{"sTyle"}}="{{"color: expression(alert(1337))"}}">`, 661 `<div ZgotmplZ="color: expression(alert(1337))">`, 662 }, 663 { 664 "bad dynamic attribute name 3", 665 // Allow title or alt, but not a URL. 666 `<img {{"src"}}="{{"javascript:doEvil()"}}">`, 667 `<img ZgotmplZ="javascript:doEvil()">`, 668 }, 669 { 670 "bad dynamic attribute name 4", 671 // Structure preservation requires values to associate 672 // with a consistent attribute. 673 `<input checked {{""}}="Whose value am I?">`, 674 `<input checked ZgotmplZ="Whose value am I?">`, 675 }, 676 { 677 "dynamic element name", 678 `<h{{3}}><table><t{{"head"}}>...</h{{3}}>`, 679 `<h3><table><thead>...</h3>`, 680 }, 681 { 682 "bad dynamic element name", 683 // Dynamic element names are typically used to switch 684 // between (thead, tfoot, tbody), (ul, ol), (th, td), 685 // and other replaceable sets. 686 // We do not currently easily support (ul, ol). 687 // If we do change to support that, this test should 688 // catch failures to filter out special tag names which 689 // would violate the structure preservation property -- 690 // if any special tag name could be substituted, then 691 // the content could be raw text/RCDATA for some inputs 692 // and regular HTML content for others. 693 `<{{"script"}}>{{"doEvil()"}}</{{"script"}}>`, 694 `<script>doEvil()</script>`, 695 }, 696 { 697 "srcset bad URL in second position", 698 `<img srcset="{{"/not-an-image#,javascript:alert(1)"}}">`, 699 // The second URL is also filtered. 700 `<img srcset="/not-an-image#,#ZgotmplZ">`, 701 }, 702 { 703 "srcset buffer growth", 704 `<img srcset={{",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"}}>`, 705 `<img srcset=,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,>`, 706 }, 707 { 708 "unquoted empty attribute value (plaintext)", 709 "<p name={{.U}}>", 710 "<p name=ZgotmplZ>", 711 }, 712 { 713 "unquoted empty attribute value (url)", 714 "<p href={{.U}}>", 715 "<p href=ZgotmplZ>", 716 }, 717 { 718 "quoted empty attribute value", 719 "<p name=\"{{.U}}\">", 720 "<p name=\"\">", 721 }, 722 { 723 "JS template lit special characters", 724 "<script>var a = `{{.I}}`</script>", 725 "<script>var a = `\\u0024\\u007b asd \\u0060\\u0060 \\u007d`</script>", 726 }, 727 { 728 "JS template lit special characters, nested lit", 729 "<script>var a = `${ `{{.I}}` }`</script>", 730 "<script>var a = `${ `\\u0024\\u007b asd \\u0060\\u0060 \\u007d` }`</script>", 731 }, 732 { 733 "JS template lit, nested JS", 734 "<script>var a = `${ var a = \"{{\"a \\\" d\"}}\" }`</script>", 735 "<script>var a = `${ var a = \"a \\u0022 d\" }`</script>", 736 }, 737 } 738 739 for _, test := range tests { 740 t.Run(test.name, func(t *testing.T) { 741 tmpl := New(test.name) 742 tmpl = Must(tmpl.Parse(test.input)) 743 // Check for bug 6459: Tree field was not set in Parse. 744 if tmpl.Tree != tmpl.text.Tree { 745 t.Fatalf("%s: tree not set properly", test.name) 746 } 747 b := new(strings.Builder) 748 if err := tmpl.Execute(b, data); err != nil { 749 t.Fatalf("%s: template execution failed: %s", test.name, err) 750 } 751 if w, g := test.output, b.String(); w != g { 752 t.Fatalf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g) 753 } 754 b.Reset() 755 if err := tmpl.Execute(b, pdata); err != nil { 756 t.Fatalf("%s: template execution failed for pointer: %s", test.name, err) 757 } 758 if w, g := test.output, b.String(); w != g { 759 t.Fatalf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g) 760 } 761 if tmpl.Tree != tmpl.text.Tree { 762 t.Fatalf("%s: tree mismatch", test.name) 763 } 764 }) 765 } 766} 767 768func TestEscapeMap(t *testing.T) { 769 data := map[string]string{ 770 "html": `<h1>Hi!</h1>`, 771 "urlquery": `http://www.foo.com/index.html?title=main`, 772 } 773 for _, test := range [...]struct { 774 desc, input, output string 775 }{ 776 // covering issue 20323 777 { 778 "field with predefined escaper name 1", 779 `{{.html | print}}`, 780 `<h1>Hi!</h1>`, 781 }, 782 // covering issue 20323 783 { 784 "field with predefined escaper name 2", 785 `{{.urlquery | print}}`, 786 `http://www.foo.com/index.html?title=main`, 787 }, 788 } { 789 tmpl := Must(New("").Parse(test.input)) 790 b := new(strings.Builder) 791 if err := tmpl.Execute(b, data); err != nil { 792 t.Errorf("%s: template execution failed: %s", test.desc, err) 793 continue 794 } 795 if w, g := test.output, b.String(); w != g { 796 t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.desc, w, g) 797 continue 798 } 799 } 800} 801 802func TestEscapeSet(t *testing.T) { 803 type dataItem struct { 804 Children []*dataItem 805 X string 806 } 807 808 data := dataItem{ 809 Children: []*dataItem{ 810 {X: "foo"}, 811 {X: "<bar>"}, 812 { 813 Children: []*dataItem{ 814 {X: "baz"}, 815 }, 816 }, 817 }, 818 } 819 820 tests := []struct { 821 inputs map[string]string 822 want string 823 }{ 824 // The trivial set. 825 { 826 map[string]string{ 827 "main": ``, 828 }, 829 ``, 830 }, 831 // A template called in the start context. 832 { 833 map[string]string{ 834 "main": `Hello, {{template "helper"}}!`, 835 // Not a valid top level HTML template. 836 // "<b" is not a full tag. 837 "helper": `{{"<World>"}}`, 838 }, 839 `Hello, <World>!`, 840 }, 841 // A template called in a context other than the start. 842 { 843 map[string]string{ 844 "main": `<a onclick='a = {{template "helper"}};'>`, 845 // Not a valid top level HTML template. 846 // "<b" is not a full tag. 847 "helper": `{{"<a>"}}<b`, 848 }, 849 `<a onclick='a = "\u003ca\u003e"<b;'>`, 850 }, 851 // A recursive template that ends in its start context. 852 { 853 map[string]string{ 854 "main": `{{range .Children}}{{template "main" .}}{{else}}{{.X}} {{end}}`, 855 }, 856 `foo <bar> baz `, 857 }, 858 // A recursive helper template that ends in its start context. 859 { 860 map[string]string{ 861 "main": `{{template "helper" .}}`, 862 "helper": `{{if .Children}}<ul>{{range .Children}}<li>{{template "main" .}}</li>{{end}}</ul>{{else}}{{.X}}{{end}}`, 863 }, 864 `<ul><li>foo</li><li><bar></li><li><ul><li>baz</li></ul></li></ul>`, 865 }, 866 // Co-recursive templates that end in its start context. 867 { 868 map[string]string{ 869 "main": `<blockquote>{{range .Children}}{{template "helper" .}}{{end}}</blockquote>`, 870 "helper": `{{if .Children}}{{template "main" .}}{{else}}{{.X}}<br>{{end}}`, 871 }, 872 `<blockquote>foo<br><bar><br><blockquote>baz<br></blockquote></blockquote>`, 873 }, 874 // A template that is called in two different contexts. 875 { 876 map[string]string{ 877 "main": `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`, 878 "helper": `{{11}} of {{"<100>"}}`, 879 }, 880 `<button onclick="title='11 of \u003c100\u003e'; ...">11 of <100></button>`, 881 }, 882 // A non-recursive template that ends in a different context. 883 // helper starts in jsCtxRegexp and ends in jsCtxDivOp. 884 { 885 map[string]string{ 886 "main": `<script>var x={{template "helper"}}/{{"42"}};</script>`, 887 "helper": "{{126}}", 888 }, 889 `<script>var x= 126 /"42";</script>`, 890 }, 891 // A recursive template that ends in a similar context. 892 { 893 map[string]string{ 894 "main": `<script>var x=[{{template "countdown" 4}}];</script>`, 895 "countdown": `{{.}}{{if .}},{{template "countdown" . | pred}}{{end}}`, 896 }, 897 `<script>var x=[ 4 , 3 , 2 , 1 , 0 ];</script>`, 898 }, 899 // A recursive template that ends in a different context. 900 /* 901 { 902 map[string]string{ 903 "main": `<a href="/foo{{template "helper" .}}">`, 904 "helper": `{{if .Children}}{{range .Children}}{{template "helper" .}}{{end}}{{else}}?x={{.X}}{{end}}`, 905 }, 906 `<a href="/foo?x=foo?x=%3cbar%3e?x=baz">`, 907 }, 908 */ 909 } 910 911 // pred is a template function that returns the predecessor of a 912 // natural number for testing recursive templates. 913 fns := FuncMap{"pred": func(a ...any) (any, error) { 914 if len(a) == 1 { 915 if i, _ := a[0].(int); i > 0 { 916 return i - 1, nil 917 } 918 } 919 return nil, fmt.Errorf("undefined pred(%v)", a) 920 }} 921 922 for _, test := range tests { 923 source := "" 924 for name, body := range test.inputs { 925 source += fmt.Sprintf("{{define %q}}%s{{end}} ", name, body) 926 } 927 tmpl, err := New("root").Funcs(fns).Parse(source) 928 if err != nil { 929 t.Errorf("error parsing %q: %v", source, err) 930 continue 931 } 932 var b strings.Builder 933 934 if err := tmpl.ExecuteTemplate(&b, "main", data); err != nil { 935 t.Errorf("%q executing %v", err.Error(), tmpl.Lookup("main")) 936 continue 937 } 938 if got := b.String(); test.want != got { 939 t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got) 940 } 941 } 942 943} 944 945func TestErrors(t *testing.T) { 946 tests := []struct { 947 input string 948 err string 949 }{ 950 // Non-error cases. 951 { 952 "{{if .Cond}}<a>{{else}}<b>{{end}}", 953 "", 954 }, 955 { 956 "{{if .Cond}}<a>{{end}}", 957 "", 958 }, 959 { 960 "{{if .Cond}}{{else}}<b>{{end}}", 961 "", 962 }, 963 { 964 "{{with .Cond}}<div>{{end}}", 965 "", 966 }, 967 { 968 "{{range .Items}}<a>{{end}}", 969 "", 970 }, 971 { 972 "<a href='/foo?{{range .Items}}&{{.K}}={{.V}}{{end}}'>", 973 "", 974 }, 975 { 976 "{{range .Items}}<a{{if .X}}{{end}}>{{end}}", 977 "", 978 }, 979 { 980 "{{range .Items}}<a{{if .X}}{{end}}>{{continue}}{{end}}", 981 "", 982 }, 983 { 984 "{{range .Items}}<a{{if .X}}{{end}}>{{break}}{{end}}", 985 "", 986 }, 987 { 988 "{{range .Items}}<a{{if .X}}{{end}}>{{if .X}}{{break}}{{end}}{{end}}", 989 "", 990 }, 991 { 992 "<script>var a = `${a+b}`</script>`", 993 "", 994 }, 995 { 996 "<script>var tmpl = `asd`;</script>", 997 ``, 998 }, 999 { 1000 "<script>var tmpl = `${1}`;</script>", 1001 ``, 1002 }, 1003 { 1004 "<script>var tmpl = `${return ``}`;</script>", 1005 ``, 1006 }, 1007 { 1008 "<script>var tmpl = `${return {{.}} }`;</script>", 1009 ``, 1010 }, 1011 { 1012 "<script>var tmpl = `${ let a = {1:1} {{.}} }`;</script>", 1013 ``, 1014 }, 1015 { 1016 "<script>var tmpl = `asd ${return \"{\"}`;</script>", 1017 ``, 1018 }, 1019 1020 // Error cases. 1021 { 1022 "{{if .Cond}}<a{{end}}", 1023 "z:1:5: {{if}} branches", 1024 }, 1025 { 1026 "{{if .Cond}}\n{{else}}\n<a{{end}}", 1027 "z:1:5: {{if}} branches", 1028 }, 1029 { 1030 // Missing quote in the else branch. 1031 `{{if .Cond}}<a href="foo">{{else}}<a href="bar>{{end}}`, 1032 "z:1:5: {{if}} branches", 1033 }, 1034 { 1035 // Different kind of attribute: href implies a URL. 1036 "<a {{if .Cond}}href='{{else}}title='{{end}}{{.X}}'>", 1037 "z:1:8: {{if}} branches", 1038 }, 1039 { 1040 "\n{{with .X}}<a{{end}}", 1041 "z:2:7: {{with}} branches", 1042 }, 1043 { 1044 "\n{{with .X}}<a>{{else}}<a{{end}}", 1045 "z:2:7: {{with}} branches", 1046 }, 1047 { 1048 "{{range .Items}}<a{{end}}", 1049 `z:1: on range loop re-entry: "<" in attribute name: "<a"`, 1050 }, 1051 { 1052 "\n{{range .Items}} x='<a{{end}}", 1053 "z:2:8: on range loop re-entry: {{range}} branches", 1054 }, 1055 { 1056 "{{range .Items}}<a{{if .X}}{{break}}{{end}}>{{end}}", 1057 "z:1:29: at range loop break: {{range}} branches end in different contexts", 1058 }, 1059 { 1060 "{{range .Items}}<a{{if .X}}{{continue}}{{end}}>{{end}}", 1061 "z:1:29: at range loop continue: {{range}} branches end in different contexts", 1062 }, 1063 { 1064 "<a b=1 c={{.H}}", 1065 "z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd", 1066 }, 1067 { 1068 "<script>foo();", 1069 "z: ends in a non-text context: {stateJS", 1070 }, 1071 { 1072 `<a href="{{if .F}}/foo?a={{else}}/bar/{{end}}{{.H}}">`, 1073 "z:1:47: {{.H}} appears in an ambiguous context within a URL", 1074 }, 1075 { 1076 `<a onclick="alert('Hello \`, 1077 `unfinished escape sequence in JS string: "Hello \\"`, 1078 }, 1079 { 1080 `<a onclick='alert("Hello\, World\`, 1081 `unfinished escape sequence in JS string: "Hello\\, World\\"`, 1082 }, 1083 { 1084 `<a onclick='alert(/x+\`, 1085 `unfinished escape sequence in JS string: "x+\\"`, 1086 }, 1087 { 1088 `<a onclick="/foo[\]/`, 1089 `unfinished JS regexp charset: "foo[\\]/"`, 1090 }, 1091 { 1092 // It is ambiguous whether 1.5 should be 1\.5 or 1.5. 1093 // Either `var x = 1/- 1.5 /i.test(x)` 1094 // where `i.test(x)` is a method call of reference i, 1095 // or `/-1\.5/i.test(x)` which is a method call on a 1096 // case insensitive regular expression. 1097 `<script>{{if false}}var x = 1{{end}}/-{{"1.5"}}/i.test(x)</script>`, 1098 `'/' could start a division or regexp: "/-"`, 1099 }, 1100 { 1101 `{{template "foo"}}`, 1102 "z:1:11: no such template \"foo\"", 1103 }, 1104 { 1105 `<div{{template "y"}}>` + 1106 // Illegal starting in stateTag but not in stateText. 1107 `{{define "y"}} foo<b{{end}}`, 1108 `"<" in attribute name: " foo<b"`, 1109 }, 1110 { 1111 `<script>reverseList = [{{template "t"}}]</script>` + 1112 // Missing " after recursive call. 1113 `{{define "t"}}{{if .Tail}}{{template "t" .Tail}}{{end}}{{.Head}}",{{end}}`, 1114 `: cannot compute output context for template t$htmltemplate_stateJS_elementScript`, 1115 }, 1116 { 1117 `<input type=button value=onclick=>`, 1118 `html/template:z: "=" in unquoted attr: "onclick="`, 1119 }, 1120 { 1121 `<input type=button value= onclick=>`, 1122 `html/template:z: "=" in unquoted attr: "onclick="`, 1123 }, 1124 { 1125 `<input type=button value= 1+1=2>`, 1126 `html/template:z: "=" in unquoted attr: "1+1=2"`, 1127 }, 1128 { 1129 "<a class=`foo>", 1130 "html/template:z: \"`\" in unquoted attr: \"`foo\"", 1131 }, 1132 { 1133 `<a style=font:'Arial'>`, 1134 `html/template:z: "'" in unquoted attr: "font:'Arial'"`, 1135 }, 1136 { 1137 `<a=foo>`, 1138 `: expected space, attr name, or end of tag, but got "=foo>"`, 1139 }, 1140 { 1141 `Hello, {{. | urlquery | print}}!`, 1142 // urlquery is disallowed if it is not the last command in the pipeline. 1143 `predefined escaper "urlquery" disallowed in template`, 1144 }, 1145 { 1146 `Hello, {{. | html | print}}!`, 1147 // html is disallowed if it is not the last command in the pipeline. 1148 `predefined escaper "html" disallowed in template`, 1149 }, 1150 { 1151 `Hello, {{html . | print}}!`, 1152 // A direct call to html is disallowed if it is not the last command in the pipeline. 1153 `predefined escaper "html" disallowed in template`, 1154 }, 1155 { 1156 `<div class={{. | html}}>Hello<div>`, 1157 // html is disallowed in a pipeline that is in an unquoted attribute context, 1158 // even if it is the last command in the pipeline. 1159 `predefined escaper "html" disallowed in template`, 1160 }, 1161 { 1162 `Hello, {{. | urlquery | html}}!`, 1163 // html is allowed since it is the last command in the pipeline, but urlquery is not. 1164 `predefined escaper "urlquery" disallowed in template`, 1165 }, 1166 } 1167 for _, test := range tests { 1168 buf := new(bytes.Buffer) 1169 tmpl, err := New("z").Parse(test.input) 1170 if err != nil { 1171 t.Errorf("input=%q: unexpected parse error %s\n", test.input, err) 1172 continue 1173 } 1174 err = tmpl.Execute(buf, nil) 1175 var got string 1176 if err != nil { 1177 got = err.Error() 1178 } 1179 if test.err == "" { 1180 if got != "" { 1181 t.Errorf("input=%q: unexpected error %q", test.input, got) 1182 } 1183 continue 1184 } 1185 if !strings.Contains(got, test.err) { 1186 t.Errorf("input=%q: error\n\t%q\ndoes not contain expected string\n\t%q", test.input, got, test.err) 1187 continue 1188 } 1189 // Check that we get the same error if we call Execute again. 1190 if err := tmpl.Execute(buf, nil); err == nil || err.Error() != got { 1191 t.Errorf("input=%q: unexpected error on second call %q", test.input, err) 1192 1193 } 1194 } 1195} 1196 1197func TestEscapeText(t *testing.T) { 1198 tests := []struct { 1199 input string 1200 output context 1201 }{ 1202 { 1203 ``, 1204 context{}, 1205 }, 1206 { 1207 `Hello, World!`, 1208 context{}, 1209 }, 1210 { 1211 // An orphaned "<" is OK. 1212 `I <3 Ponies!`, 1213 context{}, 1214 }, 1215 { 1216 `<a`, 1217 context{state: stateTag}, 1218 }, 1219 { 1220 `<a `, 1221 context{state: stateTag}, 1222 }, 1223 { 1224 `<a>`, 1225 context{state: stateText}, 1226 }, 1227 { 1228 `<a href`, 1229 context{state: stateAttrName, attr: attrURL}, 1230 }, 1231 { 1232 `<a on`, 1233 context{state: stateAttrName, attr: attrScript}, 1234 }, 1235 { 1236 `<a href `, 1237 context{state: stateAfterName, attr: attrURL}, 1238 }, 1239 { 1240 `<a style = `, 1241 context{state: stateBeforeValue, attr: attrStyle}, 1242 }, 1243 { 1244 `<a href=`, 1245 context{state: stateBeforeValue, attr: attrURL}, 1246 }, 1247 { 1248 `<a href=x`, 1249 context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery, attr: attrURL}, 1250 }, 1251 { 1252 `<a href=x `, 1253 context{state: stateTag}, 1254 }, 1255 { 1256 `<a href=>`, 1257 context{state: stateText}, 1258 }, 1259 { 1260 `<a href=x>`, 1261 context{state: stateText}, 1262 }, 1263 { 1264 `<a href ='`, 1265 context{state: stateURL, delim: delimSingleQuote, attr: attrURL}, 1266 }, 1267 { 1268 `<a href=''`, 1269 context{state: stateTag}, 1270 }, 1271 { 1272 `<a href= "`, 1273 context{state: stateURL, delim: delimDoubleQuote, attr: attrURL}, 1274 }, 1275 { 1276 `<a href=""`, 1277 context{state: stateTag}, 1278 }, 1279 { 1280 `<a title="`, 1281 context{state: stateAttr, delim: delimDoubleQuote}, 1282 }, 1283 { 1284 `<a HREF='http:`, 1285 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1286 }, 1287 { 1288 `<a Href='/`, 1289 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1290 }, 1291 { 1292 `<a href='"`, 1293 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1294 }, 1295 { 1296 `<a href="'`, 1297 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1298 }, 1299 { 1300 `<a href=''`, 1301 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1302 }, 1303 { 1304 `<a href=""`, 1305 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1306 }, 1307 { 1308 `<a href=""`, 1309 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1310 }, 1311 { 1312 `<a href="`, 1313 context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery, attr: attrURL}, 1314 }, 1315 { 1316 `<img alt="1">`, 1317 context{state: stateText}, 1318 }, 1319 { 1320 `<img alt="1>"`, 1321 context{state: stateTag}, 1322 }, 1323 { 1324 `<img alt="1>">`, 1325 context{state: stateText}, 1326 }, 1327 { 1328 `<input checked type="checkbox"`, 1329 context{state: stateTag}, 1330 }, 1331 { 1332 `<a onclick="`, 1333 context{state: stateJS, delim: delimDoubleQuote, attr: attrScript}, 1334 }, 1335 { 1336 `<a onclick="//foo`, 1337 context{state: stateJSLineCmt, delim: delimDoubleQuote, attr: attrScript}, 1338 }, 1339 { 1340 "<a onclick='//\n", 1341 context{state: stateJS, delim: delimSingleQuote, attr: attrScript}, 1342 }, 1343 { 1344 "<a onclick='//\r\n", 1345 context{state: stateJS, delim: delimSingleQuote, attr: attrScript}, 1346 }, 1347 { 1348 "<a onclick='//\u2028", 1349 context{state: stateJS, delim: delimSingleQuote, attr: attrScript}, 1350 }, 1351 { 1352 `<a onclick="/*`, 1353 context{state: stateJSBlockCmt, delim: delimDoubleQuote, attr: attrScript}, 1354 }, 1355 { 1356 `<a onclick="/*/`, 1357 context{state: stateJSBlockCmt, delim: delimDoubleQuote, attr: attrScript}, 1358 }, 1359 { 1360 `<a onclick="/**/`, 1361 context{state: stateJS, delim: delimDoubleQuote, attr: attrScript}, 1362 }, 1363 { 1364 `<a onkeypress=""`, 1365 context{state: stateJSDqStr, delim: delimDoubleQuote, attr: attrScript}, 1366 }, 1367 { 1368 `<a onclick='"foo"`, 1369 context{state: stateJS, delim: delimSingleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1370 }, 1371 { 1372 `<a onclick='foo'`, 1373 context{state: stateJS, delim: delimSpaceOrTagEnd, jsCtx: jsCtxDivOp, attr: attrScript}, 1374 }, 1375 { 1376 `<a onclick='foo`, 1377 context{state: stateJSSqStr, delim: delimSpaceOrTagEnd, attr: attrScript}, 1378 }, 1379 { 1380 `<a onclick=""foo'`, 1381 context{state: stateJSDqStr, delim: delimDoubleQuote, attr: attrScript}, 1382 }, 1383 { 1384 `<a onclick="'foo"`, 1385 context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, 1386 }, 1387 { 1388 "<a onclick=\"`foo", 1389 context{state: stateJSTmplLit, delim: delimDoubleQuote, attr: attrScript}, 1390 }, 1391 { 1392 `<A ONCLICK="'`, 1393 context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, 1394 }, 1395 { 1396 `<a onclick="/`, 1397 context{state: stateJSRegexp, delim: delimDoubleQuote, attr: attrScript}, 1398 }, 1399 { 1400 `<a onclick="'foo'`, 1401 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1402 }, 1403 { 1404 `<a onclick="'foo\'`, 1405 context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, 1406 }, 1407 { 1408 `<a onclick="'foo\'`, 1409 context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, 1410 }, 1411 { 1412 `<a onclick="/foo/`, 1413 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1414 }, 1415 { 1416 `<script>/foo/ /=`, 1417 context{state: stateJS, element: elementScript}, 1418 }, 1419 { 1420 `<a onclick="1 /foo`, 1421 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1422 }, 1423 { 1424 `<a onclick="1 /*c*/ /foo`, 1425 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1426 }, 1427 { 1428 `<a onclick="/foo[/]`, 1429 context{state: stateJSRegexp, delim: delimDoubleQuote, attr: attrScript}, 1430 }, 1431 { 1432 `<a onclick="/foo\/`, 1433 context{state: stateJSRegexp, delim: delimDoubleQuote, attr: attrScript}, 1434 }, 1435 { 1436 `<a onclick="/foo/`, 1437 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1438 }, 1439 { 1440 `<input checked style="`, 1441 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, 1442 }, 1443 { 1444 `<a style="//`, 1445 context{state: stateCSSLineCmt, delim: delimDoubleQuote, attr: attrStyle}, 1446 }, 1447 { 1448 `<a style="//</script>`, 1449 context{state: stateCSSLineCmt, delim: delimDoubleQuote, attr: attrStyle}, 1450 }, 1451 { 1452 "<a style='//\n", 1453 context{state: stateCSS, delim: delimSingleQuote, attr: attrStyle}, 1454 }, 1455 { 1456 "<a style='//\r", 1457 context{state: stateCSS, delim: delimSingleQuote, attr: attrStyle}, 1458 }, 1459 { 1460 `<a style="/*`, 1461 context{state: stateCSSBlockCmt, delim: delimDoubleQuote, attr: attrStyle}, 1462 }, 1463 { 1464 `<a style="/*/`, 1465 context{state: stateCSSBlockCmt, delim: delimDoubleQuote, attr: attrStyle}, 1466 }, 1467 { 1468 `<a style="/**/`, 1469 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, 1470 }, 1471 { 1472 `<a style="background: '`, 1473 context{state: stateCSSSqStr, delim: delimDoubleQuote, attr: attrStyle}, 1474 }, 1475 { 1476 `<a style="background: "`, 1477 context{state: stateCSSDqStr, delim: delimDoubleQuote, attr: attrStyle}, 1478 }, 1479 { 1480 `<a style="background: '/foo?img=`, 1481 context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag, attr: attrStyle}, 1482 }, 1483 { 1484 `<a style="background: '/`, 1485 context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1486 }, 1487 { 1488 `<a style="background: url("/`, 1489 context{state: stateCSSDqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1490 }, 1491 { 1492 `<a style="background: url('/`, 1493 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1494 }, 1495 { 1496 `<a style="background: url('/)`, 1497 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1498 }, 1499 { 1500 `<a style="background: url('/ `, 1501 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1502 }, 1503 { 1504 `<a style="background: url(/`, 1505 context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1506 }, 1507 { 1508 `<a style="background: url( `, 1509 context{state: stateCSSURL, delim: delimDoubleQuote, attr: attrStyle}, 1510 }, 1511 { 1512 `<a style="background: url( /image?name=`, 1513 context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag, attr: attrStyle}, 1514 }, 1515 { 1516 `<a style="background: url(x)`, 1517 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, 1518 }, 1519 { 1520 `<a style="background: url('x'`, 1521 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, 1522 }, 1523 { 1524 `<a style="background: url( x `, 1525 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, 1526 }, 1527 { 1528 `<!-- foo`, 1529 context{state: stateHTMLCmt}, 1530 }, 1531 { 1532 `<!-->`, 1533 context{state: stateHTMLCmt}, 1534 }, 1535 { 1536 `<!--->`, 1537 context{state: stateHTMLCmt}, 1538 }, 1539 { 1540 `<!-- foo -->`, 1541 context{state: stateText}, 1542 }, 1543 { 1544 `<script`, 1545 context{state: stateTag, element: elementScript}, 1546 }, 1547 { 1548 `<script `, 1549 context{state: stateTag, element: elementScript}, 1550 }, 1551 { 1552 `<script src="foo.js" `, 1553 context{state: stateTag, element: elementScript}, 1554 }, 1555 { 1556 `<script src='foo.js' `, 1557 context{state: stateTag, element: elementScript}, 1558 }, 1559 { 1560 `<script type=text/javascript `, 1561 context{state: stateTag, element: elementScript}, 1562 }, 1563 { 1564 `<script>`, 1565 context{state: stateJS, jsCtx: jsCtxRegexp, element: elementScript}, 1566 }, 1567 { 1568 `<script>foo`, 1569 context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, 1570 }, 1571 { 1572 `<script>foo</script>`, 1573 context{state: stateText}, 1574 }, 1575 { 1576 `<script>foo</script><!--`, 1577 context{state: stateHTMLCmt}, 1578 }, 1579 { 1580 `<script>document.write("<p>foo</p>");`, 1581 context{state: stateJS, element: elementScript}, 1582 }, 1583 { 1584 `<script>document.write("<p>foo<\/script>");`, 1585 context{state: stateJS, element: elementScript}, 1586 }, 1587 { 1588 // <script and </script tags are escaped, so </script> should not 1589 // cause us to exit the JS state. 1590 `<script>document.write("<script>alert(1)</script>");`, 1591 context{state: stateJS, element: elementScript}, 1592 }, 1593 { 1594 `<script>document.write("<script>`, 1595 context{state: stateJSDqStr, element: elementScript}, 1596 }, 1597 { 1598 `<script>document.write("<script>alert(1)</script>`, 1599 context{state: stateJSDqStr, element: elementScript}, 1600 }, 1601 { 1602 `<script>document.write("<script>alert(1)<!--`, 1603 context{state: stateJSDqStr, element: elementScript}, 1604 }, 1605 { 1606 `<script>document.write("<script>alert(1)</Script>");`, 1607 context{state: stateJS, element: elementScript}, 1608 }, 1609 { 1610 `<script>document.write("<!--");`, 1611 context{state: stateJS, element: elementScript}, 1612 }, 1613 { 1614 `<script>let a = /</script`, 1615 context{state: stateJSRegexp, element: elementScript}, 1616 }, 1617 { 1618 `<script>let a = /</script/`, 1619 context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp}, 1620 }, 1621 { 1622 `<script type="text/template">`, 1623 context{state: stateText}, 1624 }, 1625 // covering issue 19968 1626 { 1627 `<script type="TEXT/JAVASCRIPT">`, 1628 context{state: stateJS, element: elementScript}, 1629 }, 1630 // covering issue 19965 1631 { 1632 `<script TYPE="text/template">`, 1633 context{state: stateText}, 1634 }, 1635 { 1636 `<script type="notjs">`, 1637 context{state: stateText}, 1638 }, 1639 { 1640 `<Script>`, 1641 context{state: stateJS, element: elementScript}, 1642 }, 1643 { 1644 `<SCRIPT>foo`, 1645 context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, 1646 }, 1647 { 1648 `<textarea>value`, 1649 context{state: stateRCDATA, element: elementTextarea}, 1650 }, 1651 { 1652 `<textarea>value</TEXTAREA>`, 1653 context{state: stateText}, 1654 }, 1655 { 1656 `<textarea name=html><b`, 1657 context{state: stateRCDATA, element: elementTextarea}, 1658 }, 1659 { 1660 `<title>value`, 1661 context{state: stateRCDATA, element: elementTitle}, 1662 }, 1663 { 1664 `<style>value`, 1665 context{state: stateCSS, element: elementStyle}, 1666 }, 1667 { 1668 `<a xlink:href`, 1669 context{state: stateAttrName, attr: attrURL}, 1670 }, 1671 { 1672 `<a xmlns`, 1673 context{state: stateAttrName, attr: attrURL}, 1674 }, 1675 { 1676 `<a xmlns:foo`, 1677 context{state: stateAttrName, attr: attrURL}, 1678 }, 1679 { 1680 `<a xmlnsxyz`, 1681 context{state: stateAttrName}, 1682 }, 1683 { 1684 `<a data-url`, 1685 context{state: stateAttrName, attr: attrURL}, 1686 }, 1687 { 1688 `<a data-iconUri`, 1689 context{state: stateAttrName, attr: attrURL}, 1690 }, 1691 { 1692 `<a data-urlItem`, 1693 context{state: stateAttrName, attr: attrURL}, 1694 }, 1695 { 1696 `<a g:`, 1697 context{state: stateAttrName}, 1698 }, 1699 { 1700 `<a g:url`, 1701 context{state: stateAttrName, attr: attrURL}, 1702 }, 1703 { 1704 `<a g:iconUri`, 1705 context{state: stateAttrName, attr: attrURL}, 1706 }, 1707 { 1708 `<a g:urlItem`, 1709 context{state: stateAttrName, attr: attrURL}, 1710 }, 1711 { 1712 `<a g:value`, 1713 context{state: stateAttrName}, 1714 }, 1715 { 1716 `<a svg:style='`, 1717 context{state: stateCSS, delim: delimSingleQuote, attr: attrStyle}, 1718 }, 1719 { 1720 `<svg:font-face`, 1721 context{state: stateTag}, 1722 }, 1723 { 1724 `<svg:a svg:onclick="`, 1725 context{state: stateJS, delim: delimDoubleQuote, attr: attrScript}, 1726 }, 1727 { 1728 `<svg:a svg:onclick="x()">`, 1729 context{}, 1730 }, 1731 { 1732 "<script>var a = `", 1733 context{state: stateJSTmplLit, element: elementScript}, 1734 }, 1735 { 1736 "<script>var a = `${", 1737 context{state: stateJS, element: elementScript}, 1738 }, 1739 { 1740 "<script>var a = `${}", 1741 context{state: stateJSTmplLit, element: elementScript}, 1742 }, 1743 { 1744 "<script>var a = `${`", 1745 context{state: stateJSTmplLit, element: elementScript}, 1746 }, 1747 { 1748 "<script>var a = `${var a = \"", 1749 context{state: stateJSDqStr, element: elementScript}, 1750 }, 1751 { 1752 "<script>var a = `${var a = \"`", 1753 context{state: stateJSDqStr, element: elementScript}, 1754 }, 1755 { 1756 "<script>var a = `${var a = \"}", 1757 context{state: stateJSDqStr, element: elementScript}, 1758 }, 1759 { 1760 "<script>var a = `${``", 1761 context{state: stateJS, element: elementScript}, 1762 }, 1763 { 1764 "<script>var a = `${`}", 1765 context{state: stateJSTmplLit, element: elementScript}, 1766 }, 1767 { 1768 "<script>`${ {} } asd`</script><script>`${ {} }", 1769 context{state: stateJSTmplLit, element: elementScript}, 1770 }, 1771 { 1772 "<script>var foo = `${ (_ => { return \"x\" })() + \"${", 1773 context{state: stateJSDqStr, element: elementScript}, 1774 }, 1775 { 1776 "<script>var a = `${ {</script><script>var b = `${ x }", 1777 context{state: stateJSTmplLit, element: elementScript, jsCtx: jsCtxDivOp}, 1778 }, 1779 { 1780 "<script>var foo = `x` + \"${", 1781 context{state: stateJSDqStr, element: elementScript}, 1782 }, 1783 { 1784 "<script>function f() { var a = `${}`; }", 1785 context{state: stateJS, element: elementScript}, 1786 }, 1787 { 1788 "<script>{`${}`}", 1789 context{state: stateJS, element: elementScript}, 1790 }, 1791 { 1792 "<script>`${ function f() { return `${1}` }() }`", 1793 context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp}, 1794 }, 1795 { 1796 "<script>function f() {`${ function f() { `${1}` } }`}", 1797 context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp}, 1798 }, 1799 { 1800 "<script>`${ { `` }", 1801 context{state: stateJS, element: elementScript}, 1802 }, 1803 { 1804 "<script>`${ { }`", 1805 context{state: stateJSTmplLit, element: elementScript}, 1806 }, 1807 { 1808 "<script>var foo = `${ foo({ a: { c: `${", 1809 context{state: stateJS, element: elementScript}, 1810 }, 1811 { 1812 "<script>var foo = `${ foo({ a: { c: `${ {{.}} }` }, b: ", 1813 context{state: stateJS, element: elementScript}, 1814 }, 1815 { 1816 "<script>`${ `}", 1817 context{state: stateJSTmplLit, element: elementScript}, 1818 }, 1819 } 1820 1821 for _, test := range tests { 1822 b, e := []byte(test.input), makeEscaper(nil) 1823 c := e.escapeText(context{}, &parse.TextNode{NodeType: parse.NodeText, Text: b}) 1824 if !test.output.eq(c) { 1825 t.Errorf("input %q: want context\n\t%v\ngot\n\t%v", test.input, test.output, c) 1826 continue 1827 } 1828 if test.input != string(b) { 1829 t.Errorf("input %q: text node was modified: want %q got %q", test.input, test.input, b) 1830 continue 1831 } 1832 } 1833} 1834 1835func TestEnsurePipelineContains(t *testing.T) { 1836 tests := []struct { 1837 input, output string 1838 ids []string 1839 }{ 1840 { 1841 "{{.X}}", 1842 ".X", 1843 []string{}, 1844 }, 1845 { 1846 "{{.X | html}}", 1847 ".X | html", 1848 []string{}, 1849 }, 1850 { 1851 "{{.X}}", 1852 ".X | html", 1853 []string{"html"}, 1854 }, 1855 { 1856 "{{html .X}}", 1857 "_eval_args_ .X | html | urlquery", 1858 []string{"html", "urlquery"}, 1859 }, 1860 { 1861 "{{html .X .Y .Z}}", 1862 "_eval_args_ .X .Y .Z | html | urlquery", 1863 []string{"html", "urlquery"}, 1864 }, 1865 { 1866 "{{.X | print}}", 1867 ".X | print | urlquery", 1868 []string{"urlquery"}, 1869 }, 1870 { 1871 "{{.X | print | urlquery}}", 1872 ".X | print | urlquery", 1873 []string{"urlquery"}, 1874 }, 1875 { 1876 "{{.X | urlquery}}", 1877 ".X | html | urlquery", 1878 []string{"html", "urlquery"}, 1879 }, 1880 { 1881 "{{.X | print 2 | .f 3}}", 1882 ".X | print 2 | .f 3 | urlquery | html", 1883 []string{"urlquery", "html"}, 1884 }, 1885 { 1886 // covering issue 10801 1887 "{{.X | println.x }}", 1888 ".X | println.x | urlquery | html", 1889 []string{"urlquery", "html"}, 1890 }, 1891 { 1892 // covering issue 10801 1893 "{{.X | (print 12 | println).x }}", 1894 ".X | (print 12 | println).x | urlquery | html", 1895 []string{"urlquery", "html"}, 1896 }, 1897 // The following test cases ensure that the merging of internal escapers 1898 // with the predefined "html" and "urlquery" escapers is correct. 1899 { 1900 "{{.X | urlquery}}", 1901 ".X | _html_template_urlfilter | urlquery", 1902 []string{"_html_template_urlfilter", "_html_template_urlnormalizer"}, 1903 }, 1904 { 1905 "{{.X | urlquery}}", 1906 ".X | urlquery | _html_template_urlfilter | _html_template_cssescaper", 1907 []string{"_html_template_urlfilter", "_html_template_cssescaper"}, 1908 }, 1909 { 1910 "{{.X | urlquery}}", 1911 ".X | urlquery", 1912 []string{"_html_template_urlnormalizer"}, 1913 }, 1914 { 1915 "{{.X | urlquery}}", 1916 ".X | urlquery", 1917 []string{"_html_template_urlescaper"}, 1918 }, 1919 { 1920 "{{.X | html}}", 1921 ".X | html", 1922 []string{"_html_template_htmlescaper"}, 1923 }, 1924 { 1925 "{{.X | html}}", 1926 ".X | html", 1927 []string{"_html_template_rcdataescaper"}, 1928 }, 1929 } 1930 for i, test := range tests { 1931 tmpl := template.Must(template.New("test").Parse(test.input)) 1932 action, ok := (tmpl.Tree.Root.Nodes[0].(*parse.ActionNode)) 1933 if !ok { 1934 t.Errorf("First node is not an action: %s", test.input) 1935 continue 1936 } 1937 pipe := action.Pipe 1938 originalIDs := make([]string, len(test.ids)) 1939 copy(originalIDs, test.ids) 1940 ensurePipelineContains(pipe, test.ids) 1941 got := pipe.String() 1942 if got != test.output { 1943 t.Errorf("#%d: %s, %v: want\n\t%s\ngot\n\t%s", i, test.input, originalIDs, test.output, got) 1944 } 1945 } 1946} 1947 1948func TestEscapeMalformedPipelines(t *testing.T) { 1949 tests := []string{ 1950 "{{ 0 | $ }}", 1951 "{{ 0 | $ | urlquery }}", 1952 "{{ 0 | (nil) }}", 1953 "{{ 0 | (nil) | html }}", 1954 } 1955 for _, test := range tests { 1956 var b bytes.Buffer 1957 tmpl, err := New("test").Parse(test) 1958 if err != nil { 1959 t.Errorf("failed to parse set: %q", err) 1960 } 1961 err = tmpl.Execute(&b, nil) 1962 if err == nil { 1963 t.Errorf("Expected error for %q", test) 1964 } 1965 } 1966} 1967 1968func TestEscapeErrorsNotIgnorable(t *testing.T) { 1969 var b bytes.Buffer 1970 tmpl, _ := New("dangerous").Parse("<a") 1971 err := tmpl.Execute(&b, nil) 1972 if err == nil { 1973 t.Errorf("Expected error") 1974 } else if b.Len() != 0 { 1975 t.Errorf("Emitted output despite escaping failure") 1976 } 1977} 1978 1979func TestEscapeSetErrorsNotIgnorable(t *testing.T) { 1980 var b bytes.Buffer 1981 tmpl, err := New("root").Parse(`{{define "t"}}<a{{end}}`) 1982 if err != nil { 1983 t.Errorf("failed to parse set: %q", err) 1984 } 1985 err = tmpl.ExecuteTemplate(&b, "t", nil) 1986 if err == nil { 1987 t.Errorf("Expected error") 1988 } else if b.Len() != 0 { 1989 t.Errorf("Emitted output despite escaping failure") 1990 } 1991} 1992 1993func TestRedundantFuncs(t *testing.T) { 1994 inputs := []any{ 1995 "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + 1996 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + 1997 ` !"#$%&'()*+,-./` + 1998 `0123456789:;<=>?` + 1999 `@ABCDEFGHIJKLMNO` + 2000 `PQRSTUVWXYZ[\]^_` + 2001 "`abcdefghijklmno" + 2002 "pqrstuvwxyz{|}~\x7f" + 2003 "\u00A0\u0100\u2028\u2029\ufeff\ufdec\ufffd\uffff\U0001D11E" + 2004 "&%22\\", 2005 CSS(`a[href =~ "//example.com"]#foo`), 2006 HTML(`Hello, <b>World</b> &tc!`), 2007 HTMLAttr(` dir="ltr"`), 2008 JS(`c && alert("Hello, World!");`), 2009 JSStr(`Hello, World & O'Reilly\x21`), 2010 URL(`greeting=H%69&addressee=(World)`), 2011 } 2012 2013 for n0, m := range redundantFuncs { 2014 f0 := funcMap[n0].(func(...any) string) 2015 for n1 := range m { 2016 f1 := funcMap[n1].(func(...any) string) 2017 for _, input := range inputs { 2018 want := f0(input) 2019 if got := f1(want); want != got { 2020 t.Errorf("%s %s with %T %q: want\n\t%q,\ngot\n\t%q", n0, n1, input, input, want, got) 2021 } 2022 } 2023 } 2024 } 2025} 2026 2027func TestIndirectPrint(t *testing.T) { 2028 a := 3 2029 ap := &a 2030 b := "hello" 2031 bp := &b 2032 bpp := &bp 2033 tmpl := Must(New("t").Parse(`{{.}}`)) 2034 var buf strings.Builder 2035 err := tmpl.Execute(&buf, ap) 2036 if err != nil { 2037 t.Errorf("Unexpected error: %s", err) 2038 } else if buf.String() != "3" { 2039 t.Errorf(`Expected "3"; got %q`, buf.String()) 2040 } 2041 buf.Reset() 2042 err = tmpl.Execute(&buf, bpp) 2043 if err != nil { 2044 t.Errorf("Unexpected error: %s", err) 2045 } else if buf.String() != "hello" { 2046 t.Errorf(`Expected "hello"; got %q`, buf.String()) 2047 } 2048} 2049 2050// This is a test for issue 3272. 2051func TestEmptyTemplateHTML(t *testing.T) { 2052 page := Must(New("page").ParseFiles(os.DevNull)) 2053 if err := page.ExecuteTemplate(os.Stdout, "page", "nothing"); err == nil { 2054 t.Fatal("expected error") 2055 } 2056} 2057 2058type Issue7379 int 2059 2060func (Issue7379) SomeMethod(x int) string { 2061 return fmt.Sprintf("<%d>", x) 2062} 2063 2064// This is a test for issue 7379: type assertion error caused panic, and then 2065// the code to handle the panic breaks escaping. It's hard to see the second 2066// problem once the first is fixed, but its fix is trivial so we let that go. See 2067// the discussion for issue 7379. 2068func TestPipeToMethodIsEscaped(t *testing.T) { 2069 tmpl := Must(New("x").Parse("<html>{{0 | .SomeMethod}}</html>\n")) 2070 tryExec := func() string { 2071 defer func() { 2072 panicValue := recover() 2073 if panicValue != nil { 2074 t.Errorf("panicked: %v\n", panicValue) 2075 } 2076 }() 2077 var b strings.Builder 2078 tmpl.Execute(&b, Issue7379(0)) 2079 return b.String() 2080 } 2081 for i := 0; i < 3; i++ { 2082 str := tryExec() 2083 const expect = "<html><0></html>\n" 2084 if str != expect { 2085 t.Errorf("expected %q got %q", expect, str) 2086 } 2087 } 2088} 2089 2090// Unlike text/template, html/template crashed if given an incomplete 2091// template, that is, a template that had been named but not given any content. 2092// This is issue #10204. 2093func TestErrorOnUndefined(t *testing.T) { 2094 tmpl := New("undefined") 2095 2096 err := tmpl.Execute(nil, nil) 2097 if err == nil { 2098 t.Error("expected error") 2099 } else if !strings.Contains(err.Error(), "incomplete") { 2100 t.Errorf("expected error about incomplete template; got %s", err) 2101 } 2102} 2103 2104// This covers issue #20842. 2105func TestIdempotentExecute(t *testing.T) { 2106 tmpl := Must(New(""). 2107 Parse(`{{define "main"}}<body>{{template "hello"}}</body>{{end}}`)) 2108 Must(tmpl. 2109 Parse(`{{define "hello"}}Hello, {{"Ladies & Gentlemen!"}}{{end}}`)) 2110 got := new(strings.Builder) 2111 var err error 2112 // Ensure that "hello" produces the same output when executed twice. 2113 want := "Hello, Ladies & Gentlemen!" 2114 for i := 0; i < 2; i++ { 2115 err = tmpl.ExecuteTemplate(got, "hello", nil) 2116 if err != nil { 2117 t.Errorf("unexpected error: %s", err) 2118 } 2119 if got.String() != want { 2120 t.Errorf("after executing template \"hello\", got:\n\t%q\nwant:\n\t%q\n", got.String(), want) 2121 } 2122 got.Reset() 2123 } 2124 // Ensure that the implicit re-execution of "hello" during the execution of 2125 // "main" does not cause the output of "hello" to change. 2126 err = tmpl.ExecuteTemplate(got, "main", nil) 2127 if err != nil { 2128 t.Errorf("unexpected error: %s", err) 2129 } 2130 // If the HTML escaper is added again to the action {{"Ladies & Gentlemen!"}}, 2131 // we would expected to see the ampersand overescaped to "&amp;". 2132 want = "<body>Hello, Ladies & Gentlemen!</body>" 2133 if got.String() != want { 2134 t.Errorf("after executing template \"main\", got:\n\t%q\nwant:\n\t%q\n", got.String(), want) 2135 } 2136} 2137 2138func BenchmarkEscapedExecute(b *testing.B) { 2139 tmpl := Must(New("t").Parse(`<a onclick="alert('{{.}}')">{{.}}</a>`)) 2140 var buf bytes.Buffer 2141 b.ResetTimer() 2142 for i := 0; i < b.N; i++ { 2143 tmpl.Execute(&buf, "foo & 'bar' & baz") 2144 buf.Reset() 2145 } 2146} 2147 2148// Covers issue 22780. 2149func TestOrphanedTemplate(t *testing.T) { 2150 t1 := Must(New("foo").Parse(`<a href="{{.}}">link1</a>`)) 2151 t2 := Must(t1.New("foo").Parse(`bar`)) 2152 2153 var b strings.Builder 2154 const wantError = `template: "foo" is an incomplete or empty template` 2155 if err := t1.Execute(&b, "javascript:alert(1)"); err == nil { 2156 t.Fatal("expected error executing t1") 2157 } else if gotError := err.Error(); gotError != wantError { 2158 t.Fatalf("got t1 execution error:\n\t%s\nwant:\n\t%s", gotError, wantError) 2159 } 2160 b.Reset() 2161 if err := t2.Execute(&b, nil); err != nil { 2162 t.Fatalf("error executing t2: %s", err) 2163 } 2164 const want = "bar" 2165 if got := b.String(); got != want { 2166 t.Fatalf("t2 rendered %q, want %q", got, want) 2167 } 2168} 2169 2170// Covers issue 21844. 2171func TestAliasedParseTreeDoesNotOverescape(t *testing.T) { 2172 const ( 2173 tmplText = `{{.}}` 2174 data = `<baz>` 2175 want = `<baz>` 2176 ) 2177 // Templates "foo" and "bar" both alias the same underlying parse tree. 2178 tpl := Must(New("foo").Parse(tmplText)) 2179 if _, err := tpl.AddParseTree("bar", tpl.Tree); err != nil { 2180 t.Fatalf("AddParseTree error: %v", err) 2181 } 2182 var b1, b2 strings.Builder 2183 if err := tpl.ExecuteTemplate(&b1, "foo", data); err != nil { 2184 t.Fatalf(`ExecuteTemplate failed for "foo": %v`, err) 2185 } 2186 if err := tpl.ExecuteTemplate(&b2, "bar", data); err != nil { 2187 t.Fatalf(`ExecuteTemplate failed for "foo": %v`, err) 2188 } 2189 got1, got2 := b1.String(), b2.String() 2190 if got1 != want { 2191 t.Fatalf(`Template "foo" rendered %q, want %q`, got1, want) 2192 } 2193 if got1 != got2 { 2194 t.Fatalf(`Template "foo" and "bar" rendered %q and %q respectively, expected equal values`, got1, got2) 2195 } 2196} 2197