escape_test.go (52330B)
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 "CSS comments", 513 "<style>p// paragraph\n" + 514 `{border: 1px/* color */{{"#00f"}}}</style>`, 515 "<style>p\n" + 516 "{border: 1px #00f}</style>", 517 }, 518 { 519 "JS attr block comment", 520 `<a onclick="f(""); /* alert({{.H}}) */">`, 521 // Attribute comment tests should pass if the comments 522 // are successfully elided. 523 `<a onclick="f(""); /* alert() */">`, 524 }, 525 { 526 "JS attr line comment", 527 `<a onclick="// alert({{.G}})">`, 528 `<a onclick="// alert()">`, 529 }, 530 { 531 "CSS attr block comment", 532 `<a style="/* color: {{.H}} */">`, 533 `<a style="/* color: */">`, 534 }, 535 { 536 "CSS attr line comment", 537 `<a style="// color: {{.G}}">`, 538 `<a style="// color: ">`, 539 }, 540 { 541 "HTML substitution commented out", 542 "<p><!-- {{.H}} --></p>", 543 "<p></p>", 544 }, 545 { 546 "Comment ends flush with start", 547 "<!--{{.}}--><script>/*{{.}}*///{{.}}\n</script><style>/*{{.}}*///{{.}}\n</style><a onclick='/*{{.}}*///{{.}}' style='/*{{.}}*///{{.}}'>", 548 "<script> \n</script><style> \n</style><a onclick='/**///' style='/**///'>", 549 }, 550 { 551 "typed HTML in text", 552 `{{.W}}`, 553 `¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`, 554 }, 555 { 556 "typed HTML in attribute", 557 `<div title="{{.W}}">`, 558 `<div title="¡Hello, O'World!">`, 559 }, 560 { 561 "typed HTML in script", 562 `<button onclick="alert({{.W}})">`, 563 `<button onclick="alert("\u0026iexcl;\u003cb class=\"foo\"\u003eHello\u003c/b\u003e, \u003ctextarea\u003eO'World\u003c/textarea\u003e!")">`, 564 }, 565 { 566 "typed HTML in RCDATA", 567 `<textarea>{{.W}}</textarea>`, 568 `<textarea>¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!</textarea>`, 569 }, 570 { 571 "range in textarea", 572 "<textarea>{{range .A}}{{.}}{{end}}</textarea>", 573 "<textarea><a><b></textarea>", 574 }, 575 { 576 "No tag injection", 577 `{{"10$"}}<{{"script src,evil.org/pwnd.js"}}...`, 578 `10$<script src,evil.org/pwnd.js...`, 579 }, 580 { 581 "No comment injection", 582 `<{{"!--"}}`, 583 `<!--`, 584 }, 585 { 586 "No RCDATA end tag injection", 587 `<textarea><{{"/textarea "}}...</textarea>`, 588 `<textarea></textarea ...</textarea>`, 589 }, 590 { 591 "optional attrs", 592 `<img class="{{"iconClass"}}"` + 593 `{{if .T}} id="{{"<iconId>"}}"{{end}}` + 594 // Double quotes inside if/else. 595 ` src=` + 596 `{{if .T}}"?{{"<iconPath>"}}"` + 597 `{{else}}"images/cleardot.gif"{{end}}` + 598 // Missing space before title, but it is not a 599 // part of the src attribute. 600 `{{if .T}}title="{{"<title>"}}"{{end}}` + 601 // Quotes outside if/else. 602 ` alt="` + 603 `{{if .T}}{{"<alt>"}}` + 604 `{{else}}{{if .F}}{{"<title>"}}{{end}}` + 605 `{{end}}"` + 606 `>`, 607 `<img class="iconClass" id="<iconId>" src="?%3ciconPath%3e"title="<title>" alt="<alt>">`, 608 }, 609 { 610 "conditional valueless attr name", 611 `<input{{if .T}} checked{{end}} name=n>`, 612 `<input checked name=n>`, 613 }, 614 { 615 "conditional dynamic valueless attr name 1", 616 `<input{{if .T}} {{"checked"}}{{end}} name=n>`, 617 `<input checked name=n>`, 618 }, 619 { 620 "conditional dynamic valueless attr name 2", 621 `<input {{if .T}}{{"checked"}} {{end}}name=n>`, 622 `<input checked name=n>`, 623 }, 624 { 625 "dynamic attribute name", 626 `<img on{{"load"}}="alert({{"loaded"}})">`, 627 // Treated as JS since quotes are inserted. 628 `<img onload="alert("loaded")">`, 629 }, 630 { 631 "bad dynamic attribute name 1", 632 // Allow checked, selected, disabled, but not JS or 633 // CSS attributes. 634 `<input {{"onchange"}}="{{"doEvil()"}}">`, 635 `<input ZgotmplZ="doEvil()">`, 636 }, 637 { 638 "bad dynamic attribute name 2", 639 `<div {{"sTyle"}}="{{"color: expression(alert(1337))"}}">`, 640 `<div ZgotmplZ="color: expression(alert(1337))">`, 641 }, 642 { 643 "bad dynamic attribute name 3", 644 // Allow title or alt, but not a URL. 645 `<img {{"src"}}="{{"javascript:doEvil()"}}">`, 646 `<img ZgotmplZ="javascript:doEvil()">`, 647 }, 648 { 649 "bad dynamic attribute name 4", 650 // Structure preservation requires values to associate 651 // with a consistent attribute. 652 `<input checked {{""}}="Whose value am I?">`, 653 `<input checked ZgotmplZ="Whose value am I?">`, 654 }, 655 { 656 "dynamic element name", 657 `<h{{3}}><table><t{{"head"}}>...</h{{3}}>`, 658 `<h3><table><thead>...</h3>`, 659 }, 660 { 661 "bad dynamic element name", 662 // Dynamic element names are typically used to switch 663 // between (thead, tfoot, tbody), (ul, ol), (th, td), 664 // and other replaceable sets. 665 // We do not currently easily support (ul, ol). 666 // If we do change to support that, this test should 667 // catch failures to filter out special tag names which 668 // would violate the structure preservation property -- 669 // if any special tag name could be substituted, then 670 // the content could be raw text/RCDATA for some inputs 671 // and regular HTML content for others. 672 `<{{"script"}}>{{"doEvil()"}}</{{"script"}}>`, 673 `<script>doEvil()</script>`, 674 }, 675 { 676 "srcset bad URL in second position", 677 `<img srcset="{{"/not-an-image#,javascript:alert(1)"}}">`, 678 // The second URL is also filtered. 679 `<img srcset="/not-an-image#,#ZgotmplZ">`, 680 }, 681 { 682 "srcset buffer growth", 683 `<img srcset={{",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"}}>`, 684 `<img srcset=,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,>`, 685 }, 686 } 687 688 for _, test := range tests { 689 tmpl := New(test.name) 690 tmpl = Must(tmpl.Parse(test.input)) 691 // Check for bug 6459: Tree field was not set in Parse. 692 if tmpl.Tree != tmpl.text.Tree { 693 t.Errorf("%s: tree not set properly", test.name) 694 continue 695 } 696 b := new(bytes.Buffer) 697 if err := tmpl.Execute(b, data); err != nil { 698 t.Errorf("%s: template execution failed: %s", test.name, err) 699 continue 700 } 701 if w, g := test.output, b.String(); w != g { 702 t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g) 703 continue 704 } 705 b.Reset() 706 if err := tmpl.Execute(b, pdata); err != nil { 707 t.Errorf("%s: template execution failed for pointer: %s", test.name, err) 708 continue 709 } 710 if w, g := test.output, b.String(); w != g { 711 t.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g) 712 continue 713 } 714 if tmpl.Tree != tmpl.text.Tree { 715 t.Errorf("%s: tree mismatch", test.name) 716 continue 717 } 718 } 719 } 720 721 func TestEscapeMap(t *testing.T) { 722 data := map[string]string{ 723 "html": `<h1>Hi!</h1>`, 724 "urlquery": `http://www.foo.com/index.html?title=main`, 725 } 726 for _, test := range [...]struct { 727 desc, input, output string 728 }{ 729 // covering issue 20323 730 { 731 "field with predefined escaper name 1", 732 `{{.html | print}}`, 733 `<h1>Hi!</h1>`, 734 }, 735 // covering issue 20323 736 { 737 "field with predefined escaper name 2", 738 `{{.urlquery | print}}`, 739 `http://www.foo.com/index.html?title=main`, 740 }, 741 } { 742 tmpl := Must(New("").Parse(test.input)) 743 b := new(bytes.Buffer) 744 if err := tmpl.Execute(b, data); err != nil { 745 t.Errorf("%s: template execution failed: %s", test.desc, err) 746 continue 747 } 748 if w, g := test.output, b.String(); w != g { 749 t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.desc, w, g) 750 continue 751 } 752 } 753 } 754 755 func TestEscapeSet(t *testing.T) { 756 type dataItem struct { 757 Children []*dataItem 758 X string 759 } 760 761 data := dataItem{ 762 Children: []*dataItem{ 763 {X: "foo"}, 764 {X: "<bar>"}, 765 { 766 Children: []*dataItem{ 767 {X: "baz"}, 768 }, 769 }, 770 }, 771 } 772 773 tests := []struct { 774 inputs map[string]string 775 want string 776 }{ 777 // The trivial set. 778 { 779 map[string]string{ 780 "main": ``, 781 }, 782 ``, 783 }, 784 // A template called in the start context. 785 { 786 map[string]string{ 787 "main": `Hello, {{template "helper"}}!`, 788 // Not a valid top level HTML template. 789 // "<b" is not a full tag. 790 "helper": `{{"<World>"}}`, 791 }, 792 `Hello, <World>!`, 793 }, 794 // A template called in a context other than the start. 795 { 796 map[string]string{ 797 "main": `<a onclick='a = {{template "helper"}};'>`, 798 // Not a valid top level HTML template. 799 // "<b" is not a full tag. 800 "helper": `{{"<a>"}}<b`, 801 }, 802 `<a onclick='a = "\u003ca\u003e"<b;'>`, 803 }, 804 // A recursive template that ends in its start context. 805 { 806 map[string]string{ 807 "main": `{{range .Children}}{{template "main" .}}{{else}}{{.X}} {{end}}`, 808 }, 809 `foo <bar> baz `, 810 }, 811 // A recursive helper template that ends in its start context. 812 { 813 map[string]string{ 814 "main": `{{template "helper" .}}`, 815 "helper": `{{if .Children}}<ul>{{range .Children}}<li>{{template "main" .}}</li>{{end}}</ul>{{else}}{{.X}}{{end}}`, 816 }, 817 `<ul><li>foo</li><li><bar></li><li><ul><li>baz</li></ul></li></ul>`, 818 }, 819 // Co-recursive templates that end in its start context. 820 { 821 map[string]string{ 822 "main": `<blockquote>{{range .Children}}{{template "helper" .}}{{end}}</blockquote>`, 823 "helper": `{{if .Children}}{{template "main" .}}{{else}}{{.X}}<br>{{end}}`, 824 }, 825 `<blockquote>foo<br><bar><br><blockquote>baz<br></blockquote></blockquote>`, 826 }, 827 // A template that is called in two different contexts. 828 { 829 map[string]string{ 830 "main": `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`, 831 "helper": `{{11}} of {{"<100>"}}`, 832 }, 833 `<button onclick="title='11 of \u003c100\u003e'; ...">11 of <100></button>`, 834 }, 835 // A non-recursive template that ends in a different context. 836 // helper starts in jsCtxRegexp and ends in jsCtxDivOp. 837 { 838 map[string]string{ 839 "main": `<script>var x={{template "helper"}}/{{"42"}};</script>`, 840 "helper": "{{126}}", 841 }, 842 `<script>var x= 126 /"42";</script>`, 843 }, 844 // A recursive template that ends in a similar context. 845 { 846 map[string]string{ 847 "main": `<script>var x=[{{template "countdown" 4}}];</script>`, 848 "countdown": `{{.}}{{if .}},{{template "countdown" . | pred}}{{end}}`, 849 }, 850 `<script>var x=[ 4 , 3 , 2 , 1 , 0 ];</script>`, 851 }, 852 // A recursive template that ends in a different context. 853 /* 854 { 855 map[string]string{ 856 "main": `<a href="/foo{{template "helper" .}}">`, 857 "helper": `{{if .Children}}{{range .Children}}{{template "helper" .}}{{end}}{{else}}?x={{.X}}{{end}}`, 858 }, 859 `<a href="/foo?x=foo?x=%3cbar%3e?x=baz">`, 860 }, 861 */ 862 } 863 864 // pred is a template function that returns the predecessor of a 865 // natural number for testing recursive templates. 866 fns := FuncMap{"pred": func(a ...any) (any, error) { 867 if len(a) == 1 { 868 if i, _ := a[0].(int); i > 0 { 869 return i - 1, nil 870 } 871 } 872 return nil, fmt.Errorf("undefined pred(%v)", a) 873 }} 874 875 for _, test := range tests { 876 source := "" 877 for name, body := range test.inputs { 878 source += fmt.Sprintf("{{define %q}}%s{{end}} ", name, body) 879 } 880 tmpl, err := New("root").Funcs(fns).Parse(source) 881 if err != nil { 882 t.Errorf("error parsing %q: %v", source, err) 883 continue 884 } 885 var b bytes.Buffer 886 887 if err := tmpl.ExecuteTemplate(&b, "main", data); err != nil { 888 t.Errorf("%q executing %v", err.Error(), tmpl.Lookup("main")) 889 continue 890 } 891 if got := b.String(); test.want != got { 892 t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got) 893 } 894 } 895 896 } 897 898 func TestErrors(t *testing.T) { 899 tests := []struct { 900 input string 901 err string 902 }{ 903 // Non-error cases. 904 { 905 "{{if .Cond}}<a>{{else}}<b>{{end}}", 906 "", 907 }, 908 { 909 "{{if .Cond}}<a>{{end}}", 910 "", 911 }, 912 { 913 "{{if .Cond}}{{else}}<b>{{end}}", 914 "", 915 }, 916 { 917 "{{with .Cond}}<div>{{end}}", 918 "", 919 }, 920 { 921 "{{range .Items}}<a>{{end}}", 922 "", 923 }, 924 { 925 "<a href='/foo?{{range .Items}}&{{.K}}={{.V}}{{end}}'>", 926 "", 927 }, 928 { 929 "{{range .Items}}<a{{if .X}}{{end}}>{{end}}", 930 "", 931 }, 932 { 933 "{{range .Items}}<a{{if .X}}{{end}}>{{continue}}{{end}}", 934 "", 935 }, 936 { 937 "{{range .Items}}<a{{if .X}}{{end}}>{{break}}{{end}}", 938 "", 939 }, 940 { 941 "{{range .Items}}<a{{if .X}}{{end}}>{{if .X}}{{break}}{{end}}{{end}}", 942 "", 943 }, 944 // Error cases. 945 { 946 "{{if .Cond}}<a{{end}}", 947 "z:1:5: {{if}} branches", 948 }, 949 { 950 "{{if .Cond}}\n{{else}}\n<a{{end}}", 951 "z:1:5: {{if}} branches", 952 }, 953 { 954 // Missing quote in the else branch. 955 `{{if .Cond}}<a href="foo">{{else}}<a href="bar>{{end}}`, 956 "z:1:5: {{if}} branches", 957 }, 958 { 959 // Different kind of attribute: href implies a URL. 960 "<a {{if .Cond}}href='{{else}}title='{{end}}{{.X}}'>", 961 "z:1:8: {{if}} branches", 962 }, 963 { 964 "\n{{with .X}}<a{{end}}", 965 "z:2:7: {{with}} branches", 966 }, 967 { 968 "\n{{with .X}}<a>{{else}}<a{{end}}", 969 "z:2:7: {{with}} branches", 970 }, 971 { 972 "{{range .Items}}<a{{end}}", 973 `z:1: on range loop re-entry: "<" in attribute name: "<a"`, 974 }, 975 { 976 "\n{{range .Items}} x='<a{{end}}", 977 "z:2:8: on range loop re-entry: {{range}} branches", 978 }, 979 { 980 "{{range .Items}}<a{{if .X}}{{break}}{{end}}>{{end}}", 981 "z:1:29: at range loop break: {{range}} branches end in different contexts", 982 }, 983 { 984 "{{range .Items}}<a{{if .X}}{{continue}}{{end}}>{{end}}", 985 "z:1:29: at range loop continue: {{range}} branches end in different contexts", 986 }, 987 { 988 "<a b=1 c={{.H}}", 989 "z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd", 990 }, 991 { 992 "<script>foo();", 993 "z: ends in a non-text context: {stateJS", 994 }, 995 { 996 `<a href="{{if .F}}/foo?a={{else}}/bar/{{end}}{{.H}}">`, 997 "z:1:47: {{.H}} appears in an ambiguous context within a URL", 998 }, 999 { 1000 `<a onclick="alert('Hello \`, 1001 `unfinished escape sequence in JS string: "Hello \\"`, 1002 }, 1003 { 1004 `<a onclick='alert("Hello\, World\`, 1005 `unfinished escape sequence in JS string: "Hello\\, World\\"`, 1006 }, 1007 { 1008 `<a onclick='alert(/x+\`, 1009 `unfinished escape sequence in JS string: "x+\\"`, 1010 }, 1011 { 1012 `<a onclick="/foo[\]/`, 1013 `unfinished JS regexp charset: "foo[\\]/"`, 1014 }, 1015 { 1016 // It is ambiguous whether 1.5 should be 1\.5 or 1.5. 1017 // Either `var x = 1/- 1.5 /i.test(x)` 1018 // where `i.test(x)` is a method call of reference i, 1019 // or `/-1\.5/i.test(x)` which is a method call on a 1020 // case insensitive regular expression. 1021 `<script>{{if false}}var x = 1{{end}}/-{{"1.5"}}/i.test(x)</script>`, 1022 `'/' could start a division or regexp: "/-"`, 1023 }, 1024 { 1025 `{{template "foo"}}`, 1026 "z:1:11: no such template \"foo\"", 1027 }, 1028 { 1029 `<div{{template "y"}}>` + 1030 // Illegal starting in stateTag but not in stateText. 1031 `{{define "y"}} foo<b{{end}}`, 1032 `"<" in attribute name: " foo<b"`, 1033 }, 1034 { 1035 `<script>reverseList = [{{template "t"}}]</script>` + 1036 // Missing " after recursive call. 1037 `{{define "t"}}{{if .Tail}}{{template "t" .Tail}}{{end}}{{.Head}}",{{end}}`, 1038 `: cannot compute output context for template t$htmltemplate_stateJS_elementScript`, 1039 }, 1040 { 1041 `<input type=button value=onclick=>`, 1042 `html/template:z: "=" in unquoted attr: "onclick="`, 1043 }, 1044 { 1045 `<input type=button value= onclick=>`, 1046 `html/template:z: "=" in unquoted attr: "onclick="`, 1047 }, 1048 { 1049 `<input type=button value= 1+1=2>`, 1050 `html/template:z: "=" in unquoted attr: "1+1=2"`, 1051 }, 1052 { 1053 "<a class=`foo>", 1054 "html/template:z: \"`\" in unquoted attr: \"`foo\"", 1055 }, 1056 { 1057 `<a style=font:'Arial'>`, 1058 `html/template:z: "'" in unquoted attr: "font:'Arial'"`, 1059 }, 1060 { 1061 `<a=foo>`, 1062 `: expected space, attr name, or end of tag, but got "=foo>"`, 1063 }, 1064 { 1065 `Hello, {{. | urlquery | print}}!`, 1066 // urlquery is disallowed if it is not the last command in the pipeline. 1067 `predefined escaper "urlquery" disallowed in template`, 1068 }, 1069 { 1070 `Hello, {{. | html | print}}!`, 1071 // html is disallowed if it is not the last command in the pipeline. 1072 `predefined escaper "html" disallowed in template`, 1073 }, 1074 { 1075 `Hello, {{html . | print}}!`, 1076 // A direct call to html is disallowed if it is not the last command in the pipeline. 1077 `predefined escaper "html" disallowed in template`, 1078 }, 1079 { 1080 `<div class={{. | html}}>Hello<div>`, 1081 // html is disallowed in a pipeline that is in an unquoted attribute context, 1082 // even if it is the last command in the pipeline. 1083 `predefined escaper "html" disallowed in template`, 1084 }, 1085 { 1086 `Hello, {{. | urlquery | html}}!`, 1087 // html is allowed since it is the last command in the pipeline, but urlquery is not. 1088 `predefined escaper "urlquery" disallowed in template`, 1089 }, 1090 } 1091 for _, test := range tests { 1092 buf := new(bytes.Buffer) 1093 tmpl, err := New("z").Parse(test.input) 1094 if err != nil { 1095 t.Errorf("input=%q: unexpected parse error %s\n", test.input, err) 1096 continue 1097 } 1098 err = tmpl.Execute(buf, nil) 1099 var got string 1100 if err != nil { 1101 got = err.Error() 1102 } 1103 if test.err == "" { 1104 if got != "" { 1105 t.Errorf("input=%q: unexpected error %q", test.input, got) 1106 } 1107 continue 1108 } 1109 if !strings.Contains(got, test.err) { 1110 t.Errorf("input=%q: error\n\t%q\ndoes not contain expected string\n\t%q", test.input, got, test.err) 1111 continue 1112 } 1113 // Check that we get the same error if we call Execute again. 1114 if err := tmpl.Execute(buf, nil); err == nil || err.Error() != got { 1115 t.Errorf("input=%q: unexpected error on second call %q", test.input, err) 1116 1117 } 1118 } 1119 } 1120 1121 func TestEscapeText(t *testing.T) { 1122 tests := []struct { 1123 input string 1124 output context 1125 }{ 1126 { 1127 ``, 1128 context{}, 1129 }, 1130 { 1131 `Hello, World!`, 1132 context{}, 1133 }, 1134 { 1135 // An orphaned "<" is OK. 1136 `I <3 Ponies!`, 1137 context{}, 1138 }, 1139 { 1140 `<a`, 1141 context{state: stateTag}, 1142 }, 1143 { 1144 `<a `, 1145 context{state: stateTag}, 1146 }, 1147 { 1148 `<a>`, 1149 context{state: stateText}, 1150 }, 1151 { 1152 `<a href`, 1153 context{state: stateAttrName, attr: attrURL}, 1154 }, 1155 { 1156 `<a on`, 1157 context{state: stateAttrName, attr: attrScript}, 1158 }, 1159 { 1160 `<a href `, 1161 context{state: stateAfterName, attr: attrURL}, 1162 }, 1163 { 1164 `<a style = `, 1165 context{state: stateBeforeValue, attr: attrStyle}, 1166 }, 1167 { 1168 `<a href=`, 1169 context{state: stateBeforeValue, attr: attrURL}, 1170 }, 1171 { 1172 `<a href=x`, 1173 context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery, attr: attrURL}, 1174 }, 1175 { 1176 `<a href=x `, 1177 context{state: stateTag}, 1178 }, 1179 { 1180 `<a href=>`, 1181 context{state: stateText}, 1182 }, 1183 { 1184 `<a href=x>`, 1185 context{state: stateText}, 1186 }, 1187 { 1188 `<a href ='`, 1189 context{state: stateURL, delim: delimSingleQuote, attr: attrURL}, 1190 }, 1191 { 1192 `<a href=''`, 1193 context{state: stateTag}, 1194 }, 1195 { 1196 `<a href= "`, 1197 context{state: stateURL, delim: delimDoubleQuote, attr: attrURL}, 1198 }, 1199 { 1200 `<a href=""`, 1201 context{state: stateTag}, 1202 }, 1203 { 1204 `<a title="`, 1205 context{state: stateAttr, delim: delimDoubleQuote}, 1206 }, 1207 { 1208 `<a HREF='http:`, 1209 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1210 }, 1211 { 1212 `<a Href='/`, 1213 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1214 }, 1215 { 1216 `<a href='"`, 1217 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1218 }, 1219 { 1220 `<a href="'`, 1221 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1222 }, 1223 { 1224 `<a href=''`, 1225 context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1226 }, 1227 { 1228 `<a href=""`, 1229 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1230 }, 1231 { 1232 `<a href=""`, 1233 context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrURL}, 1234 }, 1235 { 1236 `<a href="`, 1237 context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery, attr: attrURL}, 1238 }, 1239 { 1240 `<img alt="1">`, 1241 context{state: stateText}, 1242 }, 1243 { 1244 `<img alt="1>"`, 1245 context{state: stateTag}, 1246 }, 1247 { 1248 `<img alt="1>">`, 1249 context{state: stateText}, 1250 }, 1251 { 1252 `<input checked type="checkbox"`, 1253 context{state: stateTag}, 1254 }, 1255 { 1256 `<a onclick="`, 1257 context{state: stateJS, delim: delimDoubleQuote, attr: attrScript}, 1258 }, 1259 { 1260 `<a onclick="//foo`, 1261 context{state: stateJSLineCmt, delim: delimDoubleQuote, attr: attrScript}, 1262 }, 1263 { 1264 "<a onclick='//\n", 1265 context{state: stateJS, delim: delimSingleQuote, attr: attrScript}, 1266 }, 1267 { 1268 "<a onclick='//\r\n", 1269 context{state: stateJS, delim: delimSingleQuote, attr: attrScript}, 1270 }, 1271 { 1272 "<a onclick='//\u2028", 1273 context{state: stateJS, delim: delimSingleQuote, attr: attrScript}, 1274 }, 1275 { 1276 `<a onclick="/*`, 1277 context{state: stateJSBlockCmt, delim: delimDoubleQuote, attr: attrScript}, 1278 }, 1279 { 1280 `<a onclick="/*/`, 1281 context{state: stateJSBlockCmt, delim: delimDoubleQuote, attr: attrScript}, 1282 }, 1283 { 1284 `<a onclick="/**/`, 1285 context{state: stateJS, delim: delimDoubleQuote, attr: attrScript}, 1286 }, 1287 { 1288 `<a onkeypress=""`, 1289 context{state: stateJSDqStr, delim: delimDoubleQuote, attr: attrScript}, 1290 }, 1291 { 1292 `<a onclick='"foo"`, 1293 context{state: stateJS, delim: delimSingleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1294 }, 1295 { 1296 `<a onclick='foo'`, 1297 context{state: stateJS, delim: delimSpaceOrTagEnd, jsCtx: jsCtxDivOp, attr: attrScript}, 1298 }, 1299 { 1300 `<a onclick='foo`, 1301 context{state: stateJSSqStr, delim: delimSpaceOrTagEnd, attr: attrScript}, 1302 }, 1303 { 1304 `<a onclick=""foo'`, 1305 context{state: stateJSDqStr, delim: delimDoubleQuote, attr: attrScript}, 1306 }, 1307 { 1308 `<a onclick="'foo"`, 1309 context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, 1310 }, 1311 { 1312 `<A ONCLICK="'`, 1313 context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, 1314 }, 1315 { 1316 `<a onclick="/`, 1317 context{state: stateJSRegexp, delim: delimDoubleQuote, attr: attrScript}, 1318 }, 1319 { 1320 `<a onclick="'foo'`, 1321 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1322 }, 1323 { 1324 `<a onclick="'foo\'`, 1325 context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, 1326 }, 1327 { 1328 `<a onclick="'foo\'`, 1329 context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, 1330 }, 1331 { 1332 `<a onclick="/foo/`, 1333 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1334 }, 1335 { 1336 `<script>/foo/ /=`, 1337 context{state: stateJS, element: elementScript}, 1338 }, 1339 { 1340 `<a onclick="1 /foo`, 1341 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1342 }, 1343 { 1344 `<a onclick="1 /*c*/ /foo`, 1345 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1346 }, 1347 { 1348 `<a onclick="/foo[/]`, 1349 context{state: stateJSRegexp, delim: delimDoubleQuote, attr: attrScript}, 1350 }, 1351 { 1352 `<a onclick="/foo\/`, 1353 context{state: stateJSRegexp, delim: delimDoubleQuote, attr: attrScript}, 1354 }, 1355 { 1356 `<a onclick="/foo/`, 1357 context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, 1358 }, 1359 { 1360 `<input checked style="`, 1361 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, 1362 }, 1363 { 1364 `<a style="//`, 1365 context{state: stateCSSLineCmt, delim: delimDoubleQuote, attr: attrStyle}, 1366 }, 1367 { 1368 `<a style="//</script>`, 1369 context{state: stateCSSLineCmt, delim: delimDoubleQuote, attr: attrStyle}, 1370 }, 1371 { 1372 "<a style='//\n", 1373 context{state: stateCSS, delim: delimSingleQuote, attr: attrStyle}, 1374 }, 1375 { 1376 "<a style='//\r", 1377 context{state: stateCSS, delim: delimSingleQuote, attr: attrStyle}, 1378 }, 1379 { 1380 `<a style="/*`, 1381 context{state: stateCSSBlockCmt, delim: delimDoubleQuote, attr: attrStyle}, 1382 }, 1383 { 1384 `<a style="/*/`, 1385 context{state: stateCSSBlockCmt, delim: delimDoubleQuote, attr: attrStyle}, 1386 }, 1387 { 1388 `<a style="/**/`, 1389 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, 1390 }, 1391 { 1392 `<a style="background: '`, 1393 context{state: stateCSSSqStr, delim: delimDoubleQuote, attr: attrStyle}, 1394 }, 1395 { 1396 `<a style="background: "`, 1397 context{state: stateCSSDqStr, delim: delimDoubleQuote, attr: attrStyle}, 1398 }, 1399 { 1400 `<a style="background: '/foo?img=`, 1401 context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag, attr: attrStyle}, 1402 }, 1403 { 1404 `<a style="background: '/`, 1405 context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1406 }, 1407 { 1408 `<a style="background: url("/`, 1409 context{state: stateCSSDqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1410 }, 1411 { 1412 `<a style="background: url('/`, 1413 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1414 }, 1415 { 1416 `<a style="background: url('/)`, 1417 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1418 }, 1419 { 1420 `<a style="background: url('/ `, 1421 context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1422 }, 1423 { 1424 `<a style="background: url(/`, 1425 context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, 1426 }, 1427 { 1428 `<a style="background: url( `, 1429 context{state: stateCSSURL, delim: delimDoubleQuote, attr: attrStyle}, 1430 }, 1431 { 1432 `<a style="background: url( /image?name=`, 1433 context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag, attr: attrStyle}, 1434 }, 1435 { 1436 `<a style="background: url(x)`, 1437 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, 1438 }, 1439 { 1440 `<a style="background: url('x'`, 1441 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, 1442 }, 1443 { 1444 `<a style="background: url( x `, 1445 context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, 1446 }, 1447 { 1448 `<!-- foo`, 1449 context{state: stateHTMLCmt}, 1450 }, 1451 { 1452 `<!-->`, 1453 context{state: stateHTMLCmt}, 1454 }, 1455 { 1456 `<!--->`, 1457 context{state: stateHTMLCmt}, 1458 }, 1459 { 1460 `<!-- foo -->`, 1461 context{state: stateText}, 1462 }, 1463 { 1464 `<script`, 1465 context{state: stateTag, element: elementScript}, 1466 }, 1467 { 1468 `<script `, 1469 context{state: stateTag, element: elementScript}, 1470 }, 1471 { 1472 `<script src="foo.js" `, 1473 context{state: stateTag, element: elementScript}, 1474 }, 1475 { 1476 `<script src='foo.js' `, 1477 context{state: stateTag, element: elementScript}, 1478 }, 1479 { 1480 `<script type=text/javascript `, 1481 context{state: stateTag, element: elementScript}, 1482 }, 1483 { 1484 `<script>`, 1485 context{state: stateJS, jsCtx: jsCtxRegexp, element: elementScript}, 1486 }, 1487 { 1488 `<script>foo`, 1489 context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, 1490 }, 1491 { 1492 `<script>foo</script>`, 1493 context{state: stateText}, 1494 }, 1495 { 1496 `<script>foo</script><!--`, 1497 context{state: stateHTMLCmt}, 1498 }, 1499 { 1500 `<script>document.write("<p>foo</p>");`, 1501 context{state: stateJS, element: elementScript}, 1502 }, 1503 { 1504 `<script>document.write("<p>foo<\/script>");`, 1505 context{state: stateJS, element: elementScript}, 1506 }, 1507 { 1508 `<script>document.write("<script>alert(1)</script>");`, 1509 context{state: stateText}, 1510 }, 1511 { 1512 `<script type="text/template">`, 1513 context{state: stateText}, 1514 }, 1515 // covering issue 19968 1516 { 1517 `<script type="TEXT/JAVASCRIPT">`, 1518 context{state: stateJS, element: elementScript}, 1519 }, 1520 // covering issue 19965 1521 { 1522 `<script TYPE="text/template">`, 1523 context{state: stateText}, 1524 }, 1525 { 1526 `<script type="notjs">`, 1527 context{state: stateText}, 1528 }, 1529 { 1530 `<Script>`, 1531 context{state: stateJS, element: elementScript}, 1532 }, 1533 { 1534 `<SCRIPT>foo`, 1535 context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, 1536 }, 1537 { 1538 `<textarea>value`, 1539 context{state: stateRCDATA, element: elementTextarea}, 1540 }, 1541 { 1542 `<textarea>value</TEXTAREA>`, 1543 context{state: stateText}, 1544 }, 1545 { 1546 `<textarea name=html><b`, 1547 context{state: stateRCDATA, element: elementTextarea}, 1548 }, 1549 { 1550 `<title>value`, 1551 context{state: stateRCDATA, element: elementTitle}, 1552 }, 1553 { 1554 `<style>value`, 1555 context{state: stateCSS, element: elementStyle}, 1556 }, 1557 { 1558 `<a xlink:href`, 1559 context{state: stateAttrName, attr: attrURL}, 1560 }, 1561 { 1562 `<a xmlns`, 1563 context{state: stateAttrName, attr: attrURL}, 1564 }, 1565 { 1566 `<a xmlns:foo`, 1567 context{state: stateAttrName, attr: attrURL}, 1568 }, 1569 { 1570 `<a xmlnsxyz`, 1571 context{state: stateAttrName}, 1572 }, 1573 { 1574 `<a data-url`, 1575 context{state: stateAttrName, attr: attrURL}, 1576 }, 1577 { 1578 `<a data-iconUri`, 1579 context{state: stateAttrName, attr: attrURL}, 1580 }, 1581 { 1582 `<a data-urlItem`, 1583 context{state: stateAttrName, attr: attrURL}, 1584 }, 1585 { 1586 `<a g:`, 1587 context{state: stateAttrName}, 1588 }, 1589 { 1590 `<a g:url`, 1591 context{state: stateAttrName, attr: attrURL}, 1592 }, 1593 { 1594 `<a g:iconUri`, 1595 context{state: stateAttrName, attr: attrURL}, 1596 }, 1597 { 1598 `<a g:urlItem`, 1599 context{state: stateAttrName, attr: attrURL}, 1600 }, 1601 { 1602 `<a g:value`, 1603 context{state: stateAttrName}, 1604 }, 1605 { 1606 `<a svg:style='`, 1607 context{state: stateCSS, delim: delimSingleQuote, attr: attrStyle}, 1608 }, 1609 { 1610 `<svg:font-face`, 1611 context{state: stateTag}, 1612 }, 1613 { 1614 `<svg:a svg:onclick="`, 1615 context{state: stateJS, delim: delimDoubleQuote, attr: attrScript}, 1616 }, 1617 { 1618 `<svg:a svg:onclick="x()">`, 1619 context{}, 1620 }, 1621 } 1622 1623 for _, test := range tests { 1624 b, e := []byte(test.input), makeEscaper(nil) 1625 c := e.escapeText(context{}, &parse.TextNode{NodeType: parse.NodeText, Text: b}) 1626 if !test.output.eq(c) { 1627 t.Errorf("input %q: want context\n\t%v\ngot\n\t%v", test.input, test.output, c) 1628 continue 1629 } 1630 if test.input != string(b) { 1631 t.Errorf("input %q: text node was modified: want %q got %q", test.input, test.input, b) 1632 continue 1633 } 1634 } 1635 } 1636 1637 func TestEnsurePipelineContains(t *testing.T) { 1638 tests := []struct { 1639 input, output string 1640 ids []string 1641 }{ 1642 { 1643 "{{.X}}", 1644 ".X", 1645 []string{}, 1646 }, 1647 { 1648 "{{.X | html}}", 1649 ".X | html", 1650 []string{}, 1651 }, 1652 { 1653 "{{.X}}", 1654 ".X | html", 1655 []string{"html"}, 1656 }, 1657 { 1658 "{{html .X}}", 1659 "_eval_args_ .X | html | urlquery", 1660 []string{"html", "urlquery"}, 1661 }, 1662 { 1663 "{{html .X .Y .Z}}", 1664 "_eval_args_ .X .Y .Z | html | urlquery", 1665 []string{"html", "urlquery"}, 1666 }, 1667 { 1668 "{{.X | print}}", 1669 ".X | print | urlquery", 1670 []string{"urlquery"}, 1671 }, 1672 { 1673 "{{.X | print | urlquery}}", 1674 ".X | print | urlquery", 1675 []string{"urlquery"}, 1676 }, 1677 { 1678 "{{.X | urlquery}}", 1679 ".X | html | urlquery", 1680 []string{"html", "urlquery"}, 1681 }, 1682 { 1683 "{{.X | print 2 | .f 3}}", 1684 ".X | print 2 | .f 3 | urlquery | html", 1685 []string{"urlquery", "html"}, 1686 }, 1687 { 1688 // covering issue 10801 1689 "{{.X | println.x }}", 1690 ".X | println.x | urlquery | html", 1691 []string{"urlquery", "html"}, 1692 }, 1693 { 1694 // covering issue 10801 1695 "{{.X | (print 12 | println).x }}", 1696 ".X | (print 12 | println).x | urlquery | html", 1697 []string{"urlquery", "html"}, 1698 }, 1699 // The following test cases ensure that the merging of internal escapers 1700 // with the predefined "html" and "urlquery" escapers is correct. 1701 { 1702 "{{.X | urlquery}}", 1703 ".X | _html_template_urlfilter | urlquery", 1704 []string{"_html_template_urlfilter", "_html_template_urlnormalizer"}, 1705 }, 1706 { 1707 "{{.X | urlquery}}", 1708 ".X | urlquery | _html_template_urlfilter | _html_template_cssescaper", 1709 []string{"_html_template_urlfilter", "_html_template_cssescaper"}, 1710 }, 1711 { 1712 "{{.X | urlquery}}", 1713 ".X | urlquery", 1714 []string{"_html_template_urlnormalizer"}, 1715 }, 1716 { 1717 "{{.X | urlquery}}", 1718 ".X | urlquery", 1719 []string{"_html_template_urlescaper"}, 1720 }, 1721 { 1722 "{{.X | html}}", 1723 ".X | html", 1724 []string{"_html_template_htmlescaper"}, 1725 }, 1726 { 1727 "{{.X | html}}", 1728 ".X | html", 1729 []string{"_html_template_rcdataescaper"}, 1730 }, 1731 } 1732 for i, test := range tests { 1733 tmpl := template.Must(template.New("test").Parse(test.input)) 1734 action, ok := (tmpl.Tree.Root.Nodes[0].(*parse.ActionNode)) 1735 if !ok { 1736 t.Errorf("First node is not an action: %s", test.input) 1737 continue 1738 } 1739 pipe := action.Pipe 1740 originalIDs := make([]string, len(test.ids)) 1741 copy(originalIDs, test.ids) 1742 ensurePipelineContains(pipe, test.ids) 1743 got := pipe.String() 1744 if got != test.output { 1745 t.Errorf("#%d: %s, %v: want\n\t%s\ngot\n\t%s", i, test.input, originalIDs, test.output, got) 1746 } 1747 } 1748 } 1749 1750 func TestEscapeMalformedPipelines(t *testing.T) { 1751 tests := []string{ 1752 "{{ 0 | $ }}", 1753 "{{ 0 | $ | urlquery }}", 1754 "{{ 0 | (nil) }}", 1755 "{{ 0 | (nil) | html }}", 1756 } 1757 for _, test := range tests { 1758 var b bytes.Buffer 1759 tmpl, err := New("test").Parse(test) 1760 if err != nil { 1761 t.Errorf("failed to parse set: %q", err) 1762 } 1763 err = tmpl.Execute(&b, nil) 1764 if err == nil { 1765 t.Errorf("Expected error for %q", test) 1766 } 1767 } 1768 } 1769 1770 func TestEscapeErrorsNotIgnorable(t *testing.T) { 1771 var b bytes.Buffer 1772 tmpl, _ := New("dangerous").Parse("<a") 1773 err := tmpl.Execute(&b, nil) 1774 if err == nil { 1775 t.Errorf("Expected error") 1776 } else if b.Len() != 0 { 1777 t.Errorf("Emitted output despite escaping failure") 1778 } 1779 } 1780 1781 func TestEscapeSetErrorsNotIgnorable(t *testing.T) { 1782 var b bytes.Buffer 1783 tmpl, err := New("root").Parse(`{{define "t"}}<a{{end}}`) 1784 if err != nil { 1785 t.Errorf("failed to parse set: %q", err) 1786 } 1787 err = tmpl.ExecuteTemplate(&b, "t", nil) 1788 if err == nil { 1789 t.Errorf("Expected error") 1790 } else if b.Len() != 0 { 1791 t.Errorf("Emitted output despite escaping failure") 1792 } 1793 } 1794 1795 func TestRedundantFuncs(t *testing.T) { 1796 inputs := []any{ 1797 "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + 1798 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + 1799 ` !"#$%&'()*+,-./` + 1800 `0123456789:;<=>?` + 1801 `@ABCDEFGHIJKLMNO` + 1802 `PQRSTUVWXYZ[\]^_` + 1803 "`abcdefghijklmno" + 1804 "pqrstuvwxyz{|}~\x7f" + 1805 "\u00A0\u0100\u2028\u2029\ufeff\ufdec\ufffd\uffff\U0001D11E" + 1806 "&%22\\", 1807 htmltemplate.CSS(`a[href =~ "//example.com"]#foo`), 1808 htmltemplate.HTML(`Hello, <b>World</b> &tc!`), 1809 htmltemplate.HTMLAttr(` dir="ltr"`), 1810 htmltemplate.JS(`c && alert("Hello, World!");`), 1811 htmltemplate.JSStr(`Hello, World & O'Reilly\x21`), 1812 htmltemplate.URL(`greeting=H%69&addressee=(World)`), 1813 } 1814 1815 for n0, m := range redundantFuncs { 1816 f0 := funcMap[n0].(func(...any) string) 1817 for n1 := range m { 1818 f1 := funcMap[n1].(func(...any) string) 1819 for _, input := range inputs { 1820 want := f0(input) 1821 if got := f1(want); want != got { 1822 t.Errorf("%s %s with %T %q: want\n\t%q,\ngot\n\t%q", n0, n1, input, input, want, got) 1823 } 1824 } 1825 } 1826 } 1827 } 1828 1829 func TestIndirectPrint(t *testing.T) { 1830 a := 3 1831 ap := &a 1832 b := "hello" 1833 bp := &b 1834 bpp := &bp 1835 tmpl := Must(New("t").Parse(`{{.}}`)) 1836 var buf bytes.Buffer 1837 err := tmpl.Execute(&buf, ap) 1838 if err != nil { 1839 t.Errorf("Unexpected error: %s", err) 1840 } else if buf.String() != "3" { 1841 t.Errorf(`Expected "3"; got %q`, buf.String()) 1842 } 1843 buf.Reset() 1844 err = tmpl.Execute(&buf, bpp) 1845 if err != nil { 1846 t.Errorf("Unexpected error: %s", err) 1847 } else if buf.String() != "hello" { 1848 t.Errorf(`Expected "hello"; got %q`, buf.String()) 1849 } 1850 } 1851 1852 // This is a test for issue 3272. 1853 func TestEmptyTemplateHTML(t *testing.T) { 1854 page := Must(New("page").ParseFiles(os.DevNull)) 1855 if err := page.ExecuteTemplate(os.Stdout, "page", "nothing"); err == nil { 1856 t.Fatal("expected error") 1857 } 1858 } 1859 1860 type Issue7379 int 1861 1862 func (Issue7379) SomeMethod(x int) string { 1863 return fmt.Sprintf("<%d>", x) 1864 } 1865 1866 // This is a test for issue 7379: type assertion error caused panic, and then 1867 // the code to handle the panic breaks escaping. It's hard to see the second 1868 // problem once the first is fixed, but its fix is trivial so we let that go. See 1869 // the discussion for issue 7379. 1870 func TestPipeToMethodIsEscaped(t *testing.T) { 1871 tmpl := Must(New("x").Parse("<html>{{0 | .SomeMethod}}</html>\n")) 1872 tryExec := func() string { 1873 defer func() { 1874 panicValue := recover() 1875 if panicValue != nil { 1876 t.Errorf("panicked: %v\n", panicValue) 1877 } 1878 }() 1879 var b bytes.Buffer 1880 tmpl.Execute(&b, Issue7379(0)) 1881 return b.String() 1882 } 1883 for i := 0; i < 3; i++ { 1884 str := tryExec() 1885 const expect = "<html><0></html>\n" 1886 if str != expect { 1887 t.Errorf("expected %q got %q", expect, str) 1888 } 1889 } 1890 } 1891 1892 // Unlike text/template, html/template crashed if given an incomplete 1893 // template, that is, a template that had been named but not given any content. 1894 // This is issue #10204. 1895 func TestErrorOnUndefined(t *testing.T) { 1896 tmpl := New("undefined") 1897 1898 err := tmpl.Execute(nil, nil) 1899 if err == nil { 1900 t.Error("expected error") 1901 } else if !strings.Contains(err.Error(), "incomplete") { 1902 t.Errorf("expected error about incomplete template; got %s", err) 1903 } 1904 } 1905 1906 // This covers issue #20842. 1907 func TestIdempotentExecute(t *testing.T) { 1908 tmpl := Must(New(""). 1909 Parse(`{{define "main"}}<body>{{template "hello"}}</body>{{end}}`)) 1910 Must(tmpl. 1911 Parse(`{{define "hello"}}Hello, {{"Ladies & Gentlemen!"}}{{end}}`)) 1912 got := new(bytes.Buffer) 1913 var err error 1914 // Ensure that "hello" produces the same output when executed twice. 1915 want := "Hello, Ladies & Gentlemen!" 1916 for i := 0; i < 2; i++ { 1917 err = tmpl.ExecuteTemplate(got, "hello", nil) 1918 if err != nil { 1919 t.Errorf("unexpected error: %s", err) 1920 } 1921 if got.String() != want { 1922 t.Errorf("after executing template \"hello\", got:\n\t%q\nwant:\n\t%q\n", got.String(), want) 1923 } 1924 got.Reset() 1925 } 1926 // Ensure that the implicit re-execution of "hello" during the execution of 1927 // "main" does not cause the output of "hello" to change. 1928 err = tmpl.ExecuteTemplate(got, "main", nil) 1929 if err != nil { 1930 t.Errorf("unexpected error: %s", err) 1931 } 1932 // If the HTML escaper is added again to the action {{"Ladies & Gentlemen!"}}, 1933 // we would expected to see the ampersand overescaped to "&amp;". 1934 want = "<body>Hello, Ladies & Gentlemen!</body>" 1935 if got.String() != want { 1936 t.Errorf("after executing template \"main\", got:\n\t%q\nwant:\n\t%q\n", got.String(), want) 1937 } 1938 } 1939 1940 func BenchmarkEscapedExecute(b *testing.B) { 1941 tmpl := Must(New("t").Parse(`<a onclick="alert('{{.}}')">{{.}}</a>`)) 1942 var buf bytes.Buffer 1943 b.ResetTimer() 1944 for i := 0; i < b.N; i++ { 1945 tmpl.Execute(&buf, "foo & 'bar' & baz") 1946 buf.Reset() 1947 } 1948 } 1949 1950 // Covers issue 22780. 1951 func TestOrphanedTemplate(t *testing.T) { 1952 t1 := Must(New("foo").Parse(`<a href="{{.}}">link1</a>`)) 1953 t2 := Must(t1.New("foo").Parse(`bar`)) 1954 1955 var b bytes.Buffer 1956 const wantError = `template: "foo" is an incomplete or empty template` 1957 if err := t1.Execute(&b, "javascript:alert(1)"); err == nil { 1958 t.Fatal("expected error executing t1") 1959 } else if gotError := err.Error(); gotError != wantError { 1960 t.Fatalf("got t1 execution error:\n\t%s\nwant:\n\t%s", gotError, wantError) 1961 } 1962 b.Reset() 1963 if err := t2.Execute(&b, nil); err != nil { 1964 t.Fatalf("error executing t2: %s", err) 1965 } 1966 const want = "bar" 1967 if got := b.String(); got != want { 1968 t.Fatalf("t2 rendered %q, want %q", got, want) 1969 } 1970 } 1971 1972 // Covers issue 21844. 1973 func TestAliasedParseTreeDoesNotOverescape(t *testing.T) { 1974 const ( 1975 tmplText = `{{.}}` 1976 data = `<baz>` 1977 want = `<baz>` 1978 ) 1979 // Templates "foo" and "bar" both alias the same underlying parse tree. 1980 tpl := Must(New("foo").Parse(tmplText)) 1981 if _, err := tpl.AddParseTree("bar", tpl.Tree); err != nil { 1982 t.Fatalf("AddParseTree error: %v", err) 1983 } 1984 var b1, b2 bytes.Buffer 1985 if err := tpl.ExecuteTemplate(&b1, "foo", data); err != nil { 1986 t.Fatalf(`ExecuteTemplate failed for "foo": %v`, err) 1987 } 1988 if err := tpl.ExecuteTemplate(&b2, "bar", data); err != nil { 1989 t.Fatalf(`ExecuteTemplate failed for "foo": %v`, err) 1990 } 1991 got1, got2 := b1.String(), b2.String() 1992 if got1 != want { 1993 t.Fatalf(`Template "foo" rendered %q, want %q`, got1, want) 1994 } 1995 if got1 != got2 { 1996 t.Fatalf(`Template "foo" and "bar" rendered %q and %q respectively, expected equal values`, got1, got2) 1997 } 1998 }