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