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