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