| | |
| | |
| | |
| |
|
| | package template |
| |
|
| | import ( |
| | "bytes" |
| | "encoding/json" |
| | "fmt" |
| | "os" |
| | "strings" |
| | "testing" |
| | "text/template" |
| | "text/template/parse" |
| | ) |
| |
|
| | type badMarshaler struct{} |
| |
|
| | func (x *badMarshaler) MarshalJSON() ([]byte, error) { |
| | |
| | return []byte("{ foo: 'not quite valid JSON' }"), nil |
| | } |
| |
|
| | type goodMarshaler struct{} |
| |
|
| | func (x *goodMarshaler) MarshalJSON() ([]byte, error) { |
| | return []byte(`{ "<foo>": "O'Reilly" }`), nil |
| | } |
| |
|
| | func TestEscape(t *testing.T) { |
| | data := struct { |
| | F, T bool |
| | C, G, H, I string |
| | A, E []string |
| | B, M json.Marshaler |
| | N int |
| | U any |
| | Z *int |
| | W HTML |
| | }{ |
| | F: false, |
| | T: true, |
| | C: "<Cincinnati>", |
| | G: "<Goodbye>", |
| | H: "<Hello>", |
| | A: []string{"<a>", "<b>"}, |
| | E: []string{}, |
| | N: 42, |
| | B: &badMarshaler{}, |
| | M: &goodMarshaler{}, |
| | U: nil, |
| | Z: nil, |
| | W: HTML(`¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`), |
| | I: "${ asd `` }", |
| | } |
| | pdata := &data |
| |
|
| | tests := []struct { |
| | name string |
| | input string |
| | output string |
| | }{ |
| | { |
| | "if", |
| | "{{if .T}}Hello{{end}}, {{.C}}!", |
| | "Hello, <Cincinnati>!", |
| | }, |
| | { |
| | "else", |
| | "{{if .F}}{{.H}}{{else}}{{.G}}{{end}}!", |
| | "<Goodbye>!", |
| | }, |
| | { |
| | "overescaping1", |
| | "Hello, {{.C | html}}!", |
| | "Hello, <Cincinnati>!", |
| | }, |
| | { |
| | "overescaping2", |
| | "Hello, {{html .C}}!", |
| | "Hello, <Cincinnati>!", |
| | }, |
| | { |
| | "overescaping3", |
| | "{{with .C}}{{$msg := .}}Hello, {{$msg}}!{{end}}", |
| | "Hello, <Cincinnati>!", |
| | }, |
| | { |
| | "assignment", |
| | "{{if $x := .H}}{{$x}}{{end}}", |
| | "<Hello>", |
| | }, |
| | { |
| | "withBody", |
| | "{{with .H}}{{.}}{{end}}", |
| | "<Hello>", |
| | }, |
| | { |
| | "withElse", |
| | "{{with .E}}{{.}}{{else}}{{.H}}{{end}}", |
| | "<Hello>", |
| | }, |
| | { |
| | "rangeBody", |
| | "{{range .A}}{{.}}{{end}}", |
| | "<a><b>", |
| | }, |
| | { |
| | "rangeElse", |
| | "{{range .E}}{{.}}{{else}}{{.H}}{{end}}", |
| | "<Hello>", |
| | }, |
| | { |
| | "nonStringValue", |
| | "{{.T}}", |
| | "true", |
| | }, |
| | { |
| | "untypedNilValue", |
| | "{{.U}}", |
| | "", |
| | }, |
| | { |
| | "typedNilValue", |
| | "{{.Z}}", |
| | "<nil>", |
| | }, |
| | { |
| | "constant", |
| | `<a href="/search?q={{"'a<b'"}}">`, |
| | `<a href="/search?q=%27a%3cb%27">`, |
| | }, |
| | { |
| | "multipleAttrs", |
| | "<a b=1 c={{.H}}>", |
| | "<a b=1 c=<Hello>>", |
| | }, |
| | { |
| | "urlStartRel", |
| | `<a href='{{"/foo/bar?a=b&c=d"}}'>`, |
| | `<a href='/foo/bar?a=b&c=d'>`, |
| | }, |
| | { |
| | "urlStartAbsOk", |
| | `<a href='{{"http://example.com/foo/bar?a=b&c=d"}}'>`, |
| | `<a href='http://example.com/foo/bar?a=b&c=d'>`, |
| | }, |
| | { |
| | "protocolRelativeURLStart", |
| | `<a href='{{"//example.com:8000/foo/bar?a=b&c=d"}}'>`, |
| | `<a href='//example.com:8000/foo/bar?a=b&c=d'>`, |
| | }, |
| | { |
| | "pathRelativeURLStart", |
| | `<a href="{{"/javascript:80/foo/bar"}}">`, |
| | `<a href="/javascript:80/foo/bar">`, |
| | }, |
| | { |
| | "dangerousURLStart", |
| | `<a href='{{"javascript:alert(%22pwned%22)"}}'>`, |
| | `<a href='#ZgotmplZ'>`, |
| | }, |
| | { |
| | "dangerousURLStart2", |
| | `<a href=' {{"javascript:alert(%22pwned%22)"}}'>`, |
| | `<a href=' #ZgotmplZ'>`, |
| | }, |
| | { |
| | "nonHierURL", |
| | `<a href={{"mailto:Muhammed \"The Greatest\" Ali <m.ali@example.com>"}}>`, |
| | `<a href=mailto:Muhammed%20%22The%20Greatest%22%20Ali%20%3cm.ali@example.com%3e>`, |
| | }, |
| | { |
| | "urlPath", |
| | `<a href='http://{{"javascript:80"}}/foo'>`, |
| | `<a href='http://javascript:80/foo'>`, |
| | }, |
| | { |
| | "urlQuery", |
| | `<a href='/search?q={{.H}}'>`, |
| | `<a href='/search?q=%3cHello%3e'>`, |
| | }, |
| | { |
| | "urlFragment", |
| | `<a href='/faq#{{.H}}'>`, |
| | `<a href='/faq#%3cHello%3e'>`, |
| | }, |
| | { |
| | "urlBranch", |
| | `<a href="{{if .F}}/foo?a=b{{else}}/bar{{end}}">`, |
| | `<a href="/bar">`, |
| | }, |
| | { |
| | "urlBranchConflictMoot", |
| | `<a href="{{if .T}}/foo?a={{else}}/bar#{{end}}{{.C}}">`, |
| | `<a href="/foo?a=%3cCincinnati%3e">`, |
| | }, |
| | { |
| | "jsStrValue", |
| | "<button onclick='alert({{.H}})'>", |
| | `<button onclick='alert("\u003cHello\u003e")'>`, |
| | }, |
| | { |
| | "jsNumericValue", |
| | "<button onclick='alert({{.N}})'>", |
| | `<button onclick='alert( 42 )'>`, |
| | }, |
| | { |
| | "jsBoolValue", |
| | "<button onclick='alert({{.T}})'>", |
| | `<button onclick='alert( true )'>`, |
| | }, |
| | { |
| | "jsNilValueTyped", |
| | "<button onclick='alert(typeof{{.Z}})'>", |
| | `<button onclick='alert(typeof null )'>`, |
| | }, |
| | { |
| | "jsNilValueUntyped", |
| | "<button onclick='alert(typeof{{.U}})'>", |
| | `<button onclick='alert(typeof null )'>`, |
| | }, |
| | { |
| | "jsObjValue", |
| | "<button onclick='alert({{.A}})'>", |
| | `<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`, |
| | }, |
| | { |
| | "jsObjValueScript", |
| | "<script>alert({{.A}})</script>", |
| | `<script>alert(["\u003ca\u003e","\u003cb\u003e"])</script>`, |
| | }, |
| | { |
| | "jsObjValueNotOverEscaped", |
| | "<button onclick='alert({{.A | html}})'>", |
| | `<button onclick='alert(["\u003ca\u003e","\u003cb\u003e"])'>`, |
| | }, |
| | { |
| | "jsStr", |
| | "<button onclick='alert("{{.H}}")'>", |
| | `<button onclick='alert("\u003cHello\u003e")'>`, |
| | }, |
| | { |
| | "badMarshaler", |
| | `<button onclick='alert(1/{{.B}}in numbers)'>`, |
| | `<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)'>`, |
| | }, |
| | { |
| | "jsMarshaler", |
| | `<button onclick='alert({{.M}})'>`, |
| | `<button onclick='alert({"\u003cfoo\u003e":"O'Reilly"})'>`, |
| | }, |
| | { |
| | "jsStrNotUnderEscaped", |
| | "<button onclick='alert({{.C | urlquery}})'>", |
| | |
| | `<button onclick='alert("%3CCincinnati%3E")'>`, |
| | }, |
| | { |
| | "jsRe", |
| | `<button onclick='alert(/{{"foo+bar"}}/.test(""))'>`, |
| | `<button onclick='alert(/foo\u002bbar/.test(""))'>`, |
| | }, |
| | { |
| | "jsReBlank", |
| | `<script>alert(/{{""}}/.test(""));</script>`, |
| | `<script>alert(/(?:)/.test(""));</script>`, |
| | }, |
| | { |
| | "jsReAmbigOk", |
| | `<script>{{if true}}var x = 1{{end}}</script>`, |
| | |
| | |
| | `<script>var x = 1</script>`, |
| | }, |
| | { |
| | "styleBidiKeywordPassed", |
| | `<p style="dir: {{"ltr"}}">`, |
| | `<p style="dir: ltr">`, |
| | }, |
| | { |
| | "styleBidiPropNamePassed", |
| | `<p style="border-{{"left"}}: 0; border-{{"right"}}: 1in">`, |
| | `<p style="border-left: 0; border-right: 1in">`, |
| | }, |
| | { |
| | "styleExpressionBlocked", |
| | `<p style="width: {{"expression(alert(1337))"}}">`, |
| | `<p style="width: ZgotmplZ">`, |
| | }, |
| | { |
| | "styleTagSelectorPassed", |
| | `<style>{{"p"}} { color: pink }</style>`, |
| | `<style>p { color: pink }</style>`, |
| | }, |
| | { |
| | "styleIDPassed", |
| | `<style>p{{"#my-ID"}} { font: Arial }</style>`, |
| | `<style>p#my-ID { font: Arial }</style>`, |
| | }, |
| | { |
| | "styleClassPassed", |
| | `<style>p{{".my_class"}} { font: Arial }</style>`, |
| | `<style>p.my_class { font: Arial }</style>`, |
| | }, |
| | { |
| | "styleQuantityPassed", |
| | `<a style="left: {{"2em"}}; top: {{0}}">`, |
| | `<a style="left: 2em; top: 0">`, |
| | }, |
| | { |
| | "stylePctPassed", |
| | `<table style=width:{{"100%"}}>`, |
| | `<table style=width:100%>`, |
| | }, |
| | { |
| | "styleColorPassed", |
| | `<p style="color: {{"#8ff"}}; background: {{"#000"}}">`, |
| | `<p style="color: #8ff; background: #000">`, |
| | }, |
| | { |
| | "styleObfuscatedExpressionBlocked", |
| | `<p style="width: {{" e\\78preS\x00Sio/**/n(alert(1337))"}}">`, |
| | `<p style="width: ZgotmplZ">`, |
| | }, |
| | { |
| | "styleMozBindingBlocked", |
| | `<p style="{{"-moz-binding(alert(1337))"}}: ...">`, |
| | `<p style="ZgotmplZ: ...">`, |
| | }, |
| | { |
| | "styleObfuscatedMozBindingBlocked", |
| | `<p style="{{" -mo\\7a-B\x00I/**/nding(alert(1337))"}}: ...">`, |
| | `<p style="ZgotmplZ: ...">`, |
| | }, |
| | { |
| | "styleFontNameString", |
| | `<p style='font-family: "{{"Times New Roman"}}"'>`, |
| | `<p style='font-family: "Times New Roman"'>`, |
| | }, |
| | { |
| | "styleFontNameString", |
| | `<p style='font-family: "{{"Times New Roman"}}", "{{"sans-serif"}}"'>`, |
| | `<p style='font-family: "Times New Roman", "sans-serif"'>`, |
| | }, |
| | { |
| | "styleFontNameUnquoted", |
| | `<p style='font-family: {{"Times New Roman"}}'>`, |
| | `<p style='font-family: Times New Roman'>`, |
| | }, |
| | { |
| | "styleURLQueryEncoded", |
| | `<p style="background: url(/img?name={{"O'Reilly Animal(1)<2>.png"}})">`, |
| | `<p style="background: url(/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png)">`, |
| | }, |
| | { |
| | "styleQuotedURLQueryEncoded", |
| | `<p style="background: url('/img?name={{"O'Reilly Animal(1)<2>.png"}}')">`, |
| | `<p style="background: url('/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png')">`, |
| | }, |
| | { |
| | "styleStrQueryEncoded", |
| | `<p style="background: '/img?name={{"O'Reilly Animal(1)<2>.png"}}'">`, |
| | `<p style="background: '/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png'">`, |
| | }, |
| | { |
| | "styleURLBadProtocolBlocked", |
| | `<a style="background: url('{{"javascript:alert(1337)"}}')">`, |
| | `<a style="background: url('#ZgotmplZ')">`, |
| | }, |
| | { |
| | "styleStrBadProtocolBlocked", |
| | `<a style="background: '{{"vbscript:alert(1337)"}}'">`, |
| | `<a style="background: '#ZgotmplZ'">`, |
| | }, |
| | { |
| | "styleStrEncodedProtocolEncoded", |
| | `<a style="background: '{{"javascript\\3a alert(1337)"}}'">`, |
| | |
| | `<a style="background: 'javascript\\3a alert\28 1337\29 '">`, |
| | }, |
| | { |
| | "styleURLGoodProtocolPassed", |
| | `<a style="background: url('{{"http://oreilly.com/O'Reilly Animals(1)<2>;{}.html"}}')">`, |
| | `<a style="background: url('http://oreilly.com/O%27Reilly%20Animals%281%29%3c2%3e;%7b%7d.html')">`, |
| | }, |
| | { |
| | "styleStrGoodProtocolPassed", |
| | `<a style="background: '{{"http://oreilly.com/O'Reilly Animals(1)<2>;{}.html"}}'">`, |
| | `<a style="background: 'http\3a\2f\2foreilly.com\2fO\27Reilly Animals\28 1\29\3c 2\3e\3b\7b\7d.html'">`, |
| | }, |
| | { |
| | "styleURLEncodedForHTMLInAttr", |
| | `<a style="background: url('{{"/search?img=foo&size=icon"}}')">`, |
| | `<a style="background: url('/search?img=foo&size=icon')">`, |
| | }, |
| | { |
| | "styleURLNotEncodedForHTMLInCdata", |
| | `<style>body { background: url('{{"/search?img=foo&size=icon"}}') }</style>`, |
| | `<style>body { background: url('/search?img=foo&size=icon') }</style>`, |
| | }, |
| | { |
| | "styleURLMixedCase", |
| | `<p style="background: URL(#{{.H}})">`, |
| | `<p style="background: URL(#%3cHello%3e)">`, |
| | }, |
| | { |
| | "stylePropertyPairPassed", |
| | `<a style='{{"color: red"}}'>`, |
| | `<a style='color: red'>`, |
| | }, |
| | { |
| | "styleStrSpecialsEncoded", |
| | `<a style="font-family: '{{"/**/'\";:// \\"}}', "{{"/**/'\";:// \\"}}"">`, |
| | `<a style="font-family: '\2f**\2f\27\22\3b\3a\2f\2f \\', "\2f**\2f\27\22\3b\3a\2f\2f \\"">`, |
| | }, |
| | { |
| | "styleURLSpecialsEncoded", |
| | `<a style="border-image: url({{"/**/'\";:// \\"}}), url("{{"/**/'\";:// \\"}}"), url('{{"/**/'\";:// \\"}}'), 'http://www.example.com/?q={{"/**/'\";:// \\"}}''">`, |
| | `<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''">`, |
| | }, |
| | { |
| | "HTML comment", |
| | "<b>Hello, <!-- name of world -->{{.C}}</b>", |
| | "<b>Hello, <Cincinnati></b>", |
| | }, |
| | { |
| | "HTML comment not first < in text node.", |
| | "<<!-- -->!--", |
| | "<!--", |
| | }, |
| | { |
| | "HTML normalization 1", |
| | "a < b", |
| | "a < b", |
| | }, |
| | { |
| | "HTML normalization 2", |
| | "a << b", |
| | "a << b", |
| | }, |
| | { |
| | "HTML normalization 3", |
| | "a<<!-- --><!-- -->b", |
| | "a<b", |
| | }, |
| | { |
| | "HTML doctype not normalized", |
| | "<!DOCTYPE html>Hello, World!", |
| | "<!DOCTYPE html>Hello, World!", |
| | }, |
| | { |
| | "HTML doctype not case-insensitive", |
| | "<!doCtYPE htMl>Hello, World!", |
| | "<!doCtYPE htMl>Hello, World!", |
| | }, |
| | { |
| | "No doctype injection", |
| | `<!{{"DOCTYPE"}}`, |
| | "<!DOCTYPE", |
| | }, |
| | { |
| | "Split HTML comment", |
| | "<b>Hello, <!-- name of {{if .T}}city -->{{.C}}{{else}}world -->{{.W}}{{end}}</b>", |
| | "<b>Hello, <Cincinnati></b>", |
| | }, |
| | { |
| | "JS line comment", |
| | "<script>for (;;) { if (c()) break// foo not a label\n" + |
| | "foo({{.T}});}</script>", |
| | "<script>for (;;) { if (c()) break\n" + |
| | "foo( true );}</script>", |
| | }, |
| | { |
| | "JS multiline block comment", |
| | "<script>for (;;) { if (c()) break/* foo not a label\n" + |
| | " */foo({{.T}});}</script>", |
| | |
| | |
| | |
| | "<script>for (;;) { if (c()) break\n" + |
| | "foo( true );}</script>", |
| | }, |
| | { |
| | "JS single-line block comment", |
| | "<script>for (;;) {\n" + |
| | "if (c()) break/* foo a label */foo;" + |
| | "x({{.T}});}</script>", |
| | |
| | |
| | |
| | "<script>for (;;) {\n" + |
| | "if (c()) break foo;" + |
| | "x( true );}</script>", |
| | }, |
| | { |
| | "JS block comment flush with mathematical division", |
| | "<script>var a/*b*//c\nd</script>", |
| | "<script>var a /c\nd</script>", |
| | }, |
| | { |
| | "JS mixed comments", |
| | "<script>var a/*b*///c\nd</script>", |
| | "<script>var a \nd</script>", |
| | }, |
| | { |
| | "JS HTML-like comments", |
| | "<script>before <!-- beep\nbetween\nbefore-->boop\n</script>", |
| | "<script>before \nbetween\nbefore\n</script>", |
| | }, |
| | { |
| | "JS hashbang comment", |
| | "<script>#! beep\n</script>", |
| | "<script>\n</script>", |
| | }, |
| | { |
| | "Special tags in <script> string literals", |
| | `<script>var a = "asd < 123 <!-- 456 < fgh <script jkl < 789 </script"</script>`, |
| | `<script>var a = "asd < 123 \x3C!-- 456 < fgh \x3Cscript jkl < 789 \x3C/script"</script>`, |
| | }, |
| | { |
| | "Special tags in <script> string literals (mixed case)", |
| | `<script>var a = "<!-- <ScripT </ScripT"</script>`, |
| | `<script>var a = "\x3C!-- \x3CScripT \x3C/ScripT"</script>`, |
| | }, |
| | { |
| | "Special tags in <script> regex literals (mixed case)", |
| | `<script>var a = /<!-- <ScripT </ScripT/</script>`, |
| | `<script>var a = /\x3C!-- \x3CScripT \x3C/ScripT/</script>`, |
| | }, |
| | { |
| | "CSS comments", |
| | "<style>p// paragraph\n" + |
| | `{border: 1px/* color */{{"#00f"}}}</style>`, |
| | "<style>p\n" + |
| | "{border: 1px #00f}</style>", |
| | }, |
| | { |
| | "JS attr block comment", |
| | `<a onclick="f(""); /* alert({{.H}}) */">`, |
| | |
| | |
| | `<a onclick="f(""); /* alert() */">`, |
| | }, |
| | { |
| | "JS attr line comment", |
| | `<a onclick="// alert({{.G}})">`, |
| | `<a onclick="// alert()">`, |
| | }, |
| | { |
| | "CSS attr block comment", |
| | `<a style="/* color: {{.H}} */">`, |
| | `<a style="/* color: */">`, |
| | }, |
| | { |
| | "CSS attr line comment", |
| | `<a style="// color: {{.G}}">`, |
| | `<a style="// color: ">`, |
| | }, |
| | { |
| | "HTML substitution commented out", |
| | "<p><!-- {{.H}} --></p>", |
| | "<p></p>", |
| | }, |
| | { |
| | "Comment ends flush with start", |
| | "<!--{{.}}--><script>/*{{.}}*///{{.}}\n</script><style>/*{{.}}*///{{.}}\n</style><a onclick='/*{{.}}*///{{.}}' style='/*{{.}}*///{{.}}'>", |
| | "<script> \n</script><style> \n</style><a onclick='/**///' style='/**///'>", |
| | }, |
| | { |
| | "typed HTML in text", |
| | `{{.W}}`, |
| | `¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`, |
| | }, |
| | { |
| | "typed HTML in attribute", |
| | `<div title="{{.W}}">`, |
| | `<div title="¡Hello, O'World!">`, |
| | }, |
| | { |
| | "typed HTML in script", |
| | `<button onclick="alert({{.W}})">`, |
| | `<button onclick="alert("\u0026iexcl;\u003cb class=\"foo\"\u003eHello\u003c/b\u003e, \u003ctextarea\u003eO'World\u003c/textarea\u003e!")">`, |
| | }, |
| | { |
| | "typed HTML in RCDATA", |
| | `<textarea>{{.W}}</textarea>`, |
| | `<textarea>¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!</textarea>`, |
| | }, |
| | { |
| | "range in textarea", |
| | "<textarea>{{range .A}}{{.}}{{end}}</textarea>", |
| | "<textarea><a><b></textarea>", |
| | }, |
| | { |
| | "No tag injection", |
| | `{{"10$"}}<{{"script src,evil.org/pwnd.js"}}...`, |
| | `10$<script src,evil.org/pwnd.js...`, |
| | }, |
| | { |
| | "No comment injection", |
| | `<{{"!--"}}`, |
| | `<!--`, |
| | }, |
| | { |
| | "No RCDATA end tag injection", |
| | `<textarea><{{"/textarea "}}...</textarea>`, |
| | `<textarea></textarea ...</textarea>`, |
| | }, |
| | { |
| | "optional attrs", |
| | `<img class="{{"iconClass"}}"` + |
| | `{{if .T}} id="{{"<iconId>"}}"{{end}}` + |
| | |
| | ` src=` + |
| | `{{if .T}}"?{{"<iconPath>"}}"` + |
| | `{{else}}"images/cleardot.gif"{{end}}` + |
| | |
| | |
| | `{{if .T}}title="{{"<title>"}}"{{end}}` + |
| | |
| | ` alt="` + |
| | `{{if .T}}{{"<alt>"}}` + |
| | `{{else}}{{if .F}}{{"<title>"}}{{end}}` + |
| | `{{end}}"` + |
| | `>`, |
| | `<img class="iconClass" id="<iconId>" src="?%3ciconPath%3e"title="<title>" alt="<alt>">`, |
| | }, |
| | { |
| | "conditional valueless attr name", |
| | `<input{{if .T}} checked{{end}} name=n>`, |
| | `<input checked name=n>`, |
| | }, |
| | { |
| | "conditional dynamic valueless attr name 1", |
| | `<input{{if .T}} {{"checked"}}{{end}} name=n>`, |
| | `<input checked name=n>`, |
| | }, |
| | { |
| | "conditional dynamic valueless attr name 2", |
| | `<input {{if .T}}{{"checked"}} {{end}}name=n>`, |
| | `<input checked name=n>`, |
| | }, |
| | { |
| | "dynamic attribute name", |
| | `<img on{{"load"}}="alert({{"loaded"}})">`, |
| | |
| | `<img onload="alert("loaded")">`, |
| | }, |
| | { |
| | "bad dynamic attribute name 1", |
| | |
| | |
| | `<input {{"onchange"}}="{{"doEvil()"}}">`, |
| | `<input ZgotmplZ="doEvil()">`, |
| | }, |
| | { |
| | "bad dynamic attribute name 2", |
| | `<div {{"sTyle"}}="{{"color: expression(alert(1337))"}}">`, |
| | `<div ZgotmplZ="color: expression(alert(1337))">`, |
| | }, |
| | { |
| | "bad dynamic attribute name 3", |
| | |
| | `<img {{"src"}}="{{"javascript:doEvil()"}}">`, |
| | `<img ZgotmplZ="javascript:doEvil()">`, |
| | }, |
| | { |
| | "bad dynamic attribute name 4", |
| | |
| | |
| | `<input checked {{""}}="Whose value am I?">`, |
| | `<input checked ZgotmplZ="Whose value am I?">`, |
| | }, |
| | { |
| | "dynamic element name", |
| | `<h{{3}}><table><t{{"head"}}>...</h{{3}}>`, |
| | `<h3><table><thead>...</h3>`, |
| | }, |
| | { |
| | "bad dynamic element name", |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | `<{{"script"}}>{{"doEvil()"}}</{{"script"}}>`, |
| | `<script>doEvil()</script>`, |
| | }, |
| | { |
| | "srcset bad URL in second position", |
| | `<img srcset="{{"/not-an-image#,javascript:alert(1)"}}">`, |
| | |
| | `<img srcset="/not-an-image#,#ZgotmplZ">`, |
| | }, |
| | { |
| | "srcset buffer growth", |
| | `<img srcset={{",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"}}>`, |
| | `<img srcset=,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,>`, |
| | }, |
| | { |
| | "unquoted empty attribute value (plaintext)", |
| | "<p name={{.U}}>", |
| | "<p name=ZgotmplZ>", |
| | }, |
| | { |
| | "unquoted empty attribute value (url)", |
| | "<p href={{.U}}>", |
| | "<p href=ZgotmplZ>", |
| | }, |
| | { |
| | "quoted empty attribute value", |
| | "<p name=\"{{.U}}\">", |
| | "<p name=\"\">", |
| | }, |
| | { |
| | "JS template lit special characters", |
| | "<script>var a = `{{.I}}`</script>", |
| | "<script>var a = `\\u0024\\u007b asd \\u0060\\u0060 \\u007d`</script>", |
| | }, |
| | { |
| | "JS template lit special characters, nested lit", |
| | "<script>var a = `${ `{{.I}}` }`</script>", |
| | "<script>var a = `${ `\\u0024\\u007b asd \\u0060\\u0060 \\u007d` }`</script>", |
| | }, |
| | { |
| | "JS template lit, nested JS", |
| | "<script>var a = `${ var a = \"{{\"a \\\" d\"}}\" }`</script>", |
| | "<script>var a = `${ var a = \"a \\u0022 d\" }`</script>", |
| | }, |
| | } |
| |
|
| | for _, test := range tests { |
| | t.Run(test.name, func(t *testing.T) { |
| | tmpl := New(test.name) |
| | tmpl = Must(tmpl.Parse(test.input)) |
| | |
| | if tmpl.Tree != tmpl.text.Tree { |
| | t.Fatalf("%s: tree not set properly", test.name) |
| | } |
| | b := new(strings.Builder) |
| | if err := tmpl.Execute(b, data); err != nil { |
| | t.Fatalf("%s: template execution failed: %s", test.name, err) |
| | } |
| | if w, g := test.output, b.String(); w != g { |
| | t.Fatalf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g) |
| | } |
| | b.Reset() |
| | if err := tmpl.Execute(b, pdata); err != nil { |
| | t.Fatalf("%s: template execution failed for pointer: %s", test.name, err) |
| | } |
| | if w, g := test.output, b.String(); w != g { |
| | t.Fatalf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g) |
| | } |
| | if tmpl.Tree != tmpl.text.Tree { |
| | t.Fatalf("%s: tree mismatch", test.name) |
| | } |
| | }) |
| | } |
| | } |
| |
|
| | func TestEscapeMap(t *testing.T) { |
| | data := map[string]string{ |
| | "html": `<h1>Hi!</h1>`, |
| | "urlquery": `http://www.foo.com/index.html?title=main`, |
| | } |
| | for _, test := range [...]struct { |
| | desc, input, output string |
| | }{ |
| | |
| | { |
| | "field with predefined escaper name 1", |
| | `{{.html | print}}`, |
| | `<h1>Hi!</h1>`, |
| | }, |
| | |
| | { |
| | "field with predefined escaper name 2", |
| | `{{.urlquery | print}}`, |
| | `http://www.foo.com/index.html?title=main`, |
| | }, |
| | } { |
| | tmpl := Must(New("").Parse(test.input)) |
| | b := new(strings.Builder) |
| | if err := tmpl.Execute(b, data); err != nil { |
| | t.Errorf("%s: template execution failed: %s", test.desc, err) |
| | continue |
| | } |
| | if w, g := test.output, b.String(); w != g { |
| | t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.desc, w, g) |
| | continue |
| | } |
| | } |
| | } |
| |
|
| | func TestEscapeSet(t *testing.T) { |
| | type dataItem struct { |
| | Children []*dataItem |
| | X string |
| | } |
| |
|
| | data := dataItem{ |
| | Children: []*dataItem{ |
| | {X: "foo"}, |
| | {X: "<bar>"}, |
| | { |
| | Children: []*dataItem{ |
| | {X: "baz"}, |
| | }, |
| | }, |
| | }, |
| | } |
| |
|
| | tests := []struct { |
| | inputs map[string]string |
| | want string |
| | }{ |
| | |
| | { |
| | map[string]string{ |
| | "main": ``, |
| | }, |
| | ``, |
| | }, |
| | |
| | { |
| | map[string]string{ |
| | "main": `Hello, {{template "helper"}}!`, |
| | |
| | |
| | "helper": `{{"<World>"}}`, |
| | }, |
| | `Hello, <World>!`, |
| | }, |
| | |
| | { |
| | map[string]string{ |
| | "main": `<a onclick='a = {{template "helper"}};'>`, |
| | |
| | |
| | "helper": `{{"<a>"}}<b`, |
| | }, |
| | `<a onclick='a = "\u003ca\u003e"<b;'>`, |
| | }, |
| | |
| | { |
| | map[string]string{ |
| | "main": `{{range .Children}}{{template "main" .}}{{else}}{{.X}} {{end}}`, |
| | }, |
| | `foo <bar> baz `, |
| | }, |
| | |
| | { |
| | map[string]string{ |
| | "main": `{{template "helper" .}}`, |
| | "helper": `{{if .Children}}<ul>{{range .Children}}<li>{{template "main" .}}</li>{{end}}</ul>{{else}}{{.X}}{{end}}`, |
| | }, |
| | `<ul><li>foo</li><li><bar></li><li><ul><li>baz</li></ul></li></ul>`, |
| | }, |
| | |
| | { |
| | map[string]string{ |
| | "main": `<blockquote>{{range .Children}}{{template "helper" .}}{{end}}</blockquote>`, |
| | "helper": `{{if .Children}}{{template "main" .}}{{else}}{{.X}}<br>{{end}}`, |
| | }, |
| | `<blockquote>foo<br><bar><br><blockquote>baz<br></blockquote></blockquote>`, |
| | }, |
| | |
| | { |
| | map[string]string{ |
| | "main": `<button onclick="title='{{template "helper"}}'; ...">{{template "helper"}}</button>`, |
| | "helper": `{{11}} of {{"<100>"}}`, |
| | }, |
| | `<button onclick="title='11 of \u003c100\u003e'; ...">11 of <100></button>`, |
| | }, |
| | |
| | |
| | { |
| | map[string]string{ |
| | "main": `<script>var x={{template "helper"}}/{{"42"}};</script>`, |
| | "helper": "{{126}}", |
| | }, |
| | `<script>var x= 126 /"42";</script>`, |
| | }, |
| | |
| | { |
| | map[string]string{ |
| | "main": `<script>var x=[{{template "countdown" 4}}];</script>`, |
| | "countdown": `{{.}}{{if .}},{{template "countdown" . | pred}}{{end}}`, |
| | }, |
| | `<script>var x=[ 4 , 3 , 2 , 1 , 0 ];</script>`, |
| | }, |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | } |
| |
|
| | |
| | |
| | fns := FuncMap{"pred": func(a ...any) (any, error) { |
| | if len(a) == 1 { |
| | if i, _ := a[0].(int); i > 0 { |
| | return i - 1, nil |
| | } |
| | } |
| | return nil, fmt.Errorf("undefined pred(%v)", a) |
| | }} |
| |
|
| | for _, test := range tests { |
| | source := "" |
| | for name, body := range test.inputs { |
| | source += fmt.Sprintf("{{define %q}}%s{{end}} ", name, body) |
| | } |
| | tmpl, err := New("root").Funcs(fns).Parse(source) |
| | if err != nil { |
| | t.Errorf("error parsing %q: %v", source, err) |
| | continue |
| | } |
| | var b strings.Builder |
| |
|
| | if err := tmpl.ExecuteTemplate(&b, "main", data); err != nil { |
| | t.Errorf("%q executing %v", err.Error(), tmpl.Lookup("main")) |
| | continue |
| | } |
| | if got := b.String(); test.want != got { |
| | t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got) |
| | } |
| | } |
| |
|
| | } |
| |
|
| | func TestErrors(t *testing.T) { |
| | tests := []struct { |
| | input string |
| | err string |
| | }{ |
| | |
| | { |
| | "{{if .Cond}}<a>{{else}}<b>{{end}}", |
| | "", |
| | }, |
| | { |
| | "{{if .Cond}}<a>{{end}}", |
| | "", |
| | }, |
| | { |
| | "{{if .Cond}}{{else}}<b>{{end}}", |
| | "", |
| | }, |
| | { |
| | "{{with .Cond}}<div>{{end}}", |
| | "", |
| | }, |
| | { |
| | "{{range .Items}}<a>{{end}}", |
| | "", |
| | }, |
| | { |
| | "<a href='/foo?{{range .Items}}&{{.K}}={{.V}}{{end}}'>", |
| | "", |
| | }, |
| | { |
| | "{{range .Items}}<a{{if .X}}{{end}}>{{end}}", |
| | "", |
| | }, |
| | { |
| | "{{range .Items}}<a{{if .X}}{{end}}>{{continue}}{{end}}", |
| | "", |
| | }, |
| | { |
| | "{{range .Items}}<a{{if .X}}{{end}}>{{break}}{{end}}", |
| | "", |
| | }, |
| | { |
| | "{{range .Items}}<a{{if .X}}{{end}}>{{if .X}}{{break}}{{end}}{{end}}", |
| | "", |
| | }, |
| | { |
| | "<script>var a = `${a+b}`</script>`", |
| | "", |
| | }, |
| | { |
| | "<script>var tmpl = `asd`;</script>", |
| | ``, |
| | }, |
| | { |
| | "<script>var tmpl = `${1}`;</script>", |
| | ``, |
| | }, |
| | { |
| | "<script>var tmpl = `${return ``}`;</script>", |
| | ``, |
| | }, |
| | { |
| | "<script>var tmpl = `${return {{.}} }`;</script>", |
| | ``, |
| | }, |
| | { |
| | "<script>var tmpl = `${ let a = {1:1} {{.}} }`;</script>", |
| | ``, |
| | }, |
| | { |
| | "<script>var tmpl = `asd ${return \"{\"}`;</script>", |
| | ``, |
| | }, |
| |
|
| | |
| | { |
| | "{{if .Cond}}<a{{end}}", |
| | "z:1:5: {{if}} branches", |
| | }, |
| | { |
| | "{{if .Cond}}\n{{else}}\n<a{{end}}", |
| | "z:1:5: {{if}} branches", |
| | }, |
| | { |
| | |
| | `{{if .Cond}}<a href="foo">{{else}}<a href="bar>{{end}}`, |
| | "z:1:5: {{if}} branches", |
| | }, |
| | { |
| | |
| | "<a {{if .Cond}}href='{{else}}title='{{end}}{{.X}}'>", |
| | "z:1:8: {{if}} branches", |
| | }, |
| | { |
| | "\n{{with .X}}<a{{end}}", |
| | "z:2:7: {{with}} branches", |
| | }, |
| | { |
| | "\n{{with .X}}<a>{{else}}<a{{end}}", |
| | "z:2:7: {{with}} branches", |
| | }, |
| | { |
| | "{{range .Items}}<a{{end}}", |
| | `z:1: on range loop re-entry: "<" in attribute name: "<a"`, |
| | }, |
| | { |
| | "\n{{range .Items}} x='<a{{end}}", |
| | "z:2:8: on range loop re-entry: {{range}} branches", |
| | }, |
| | { |
| | "{{range .Items}}<a{{if .X}}{{break}}{{end}}>{{end}}", |
| | "z:1:29: at range loop break: {{range}} branches end in different contexts", |
| | }, |
| | { |
| | "{{range .Items}}<a{{if .X}}{{continue}}{{end}}>{{end}}", |
| | "z:1:29: at range loop continue: {{range}} branches end in different contexts", |
| | }, |
| | { |
| | "{{range .Items}}{{if .X}}{{break}}{{end}}<a{{if .Y}}{{continue}}{{end}}>{{if .Z}}{{continue}}{{end}}{{end}}", |
| | "z:1:54: at range loop continue: {{range}} branches end in different contexts", |
| | }, |
| | { |
| | "<a b=1 c={{.H}}", |
| | "z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd", |
| | }, |
| | { |
| | "<script>foo();", |
| | "z: ends in a non-text context: {stateJS", |
| | }, |
| | { |
| | `<a href="{{if .F}}/foo?a={{else}}/bar/{{end}}{{.H}}">`, |
| | "z:1:47: {{.H}} appears in an ambiguous context within a URL", |
| | }, |
| | { |
| | `<a onclick="alert('Hello \`, |
| | `unfinished escape sequence in JS string: "Hello \\"`, |
| | }, |
| | { |
| | `<a onclick='alert("Hello\, World\`, |
| | `unfinished escape sequence in JS string: "Hello\\, World\\"`, |
| | }, |
| | { |
| | `<a onclick='alert(/x+\`, |
| | `unfinished escape sequence in JS string: "x+\\"`, |
| | }, |
| | { |
| | `<a onclick="/foo[\]/`, |
| | `unfinished JS regexp charset: "foo[\\]/"`, |
| | }, |
| | { |
| | |
| | |
| | |
| | |
| | |
| | `<script>{{if false}}var x = 1{{end}}/-{{"1.5"}}/i.test(x)</script>`, |
| | `'/' could start a division or regexp: "/-"`, |
| | }, |
| | { |
| | `{{template "foo"}}`, |
| | "z:1:11: no such template \"foo\"", |
| | }, |
| | { |
| | `<div{{template "y"}}>` + |
| | |
| | `{{define "y"}} foo<b{{end}}`, |
| | `"<" in attribute name: " foo<b"`, |
| | }, |
| | { |
| | `<script>reverseList = [{{template "t"}}]</script>` + |
| | |
| | `{{define "t"}}{{if .Tail}}{{template "t" .Tail}}{{end}}{{.Head}}",{{end}}`, |
| | `: cannot compute output context for template t$htmltemplate_stateJS_elementScript`, |
| | }, |
| | { |
| | `<input type=button value=onclick=>`, |
| | `html/template:z: "=" in unquoted attr: "onclick="`, |
| | }, |
| | { |
| | `<input type=button value= onclick=>`, |
| | `html/template:z: "=" in unquoted attr: "onclick="`, |
| | }, |
| | { |
| | `<input type=button value= 1+1=2>`, |
| | `html/template:z: "=" in unquoted attr: "1+1=2"`, |
| | }, |
| | { |
| | "<a class=`foo>", |
| | "html/template:z: \"`\" in unquoted attr: \"`foo\"", |
| | }, |
| | { |
| | `<a style=font:'Arial'>`, |
| | `html/template:z: "'" in unquoted attr: "font:'Arial'"`, |
| | }, |
| | { |
| | `<a=foo>`, |
| | `: expected space, attr name, or end of tag, but got "=foo>"`, |
| | }, |
| | { |
| | `Hello, {{. | urlquery | print}}!`, |
| | |
| | `predefined escaper "urlquery" disallowed in template`, |
| | }, |
| | { |
| | `Hello, {{. | html | print}}!`, |
| | |
| | `predefined escaper "html" disallowed in template`, |
| | }, |
| | { |
| | `Hello, {{html . | print}}!`, |
| | |
| | `predefined escaper "html" disallowed in template`, |
| | }, |
| | { |
| | `<div class={{. | html}}>Hello<div>`, |
| | |
| | |
| | `predefined escaper "html" disallowed in template`, |
| | }, |
| | { |
| | `Hello, {{. | urlquery | html}}!`, |
| | |
| | `predefined escaper "urlquery" disallowed in template`, |
| | }, |
| | } |
| | for _, test := range tests { |
| | buf := new(bytes.Buffer) |
| | tmpl, err := New("z").Parse(test.input) |
| | if err != nil { |
| | t.Errorf("input=%q: unexpected parse error %s\n", test.input, err) |
| | continue |
| | } |
| | err = tmpl.Execute(buf, nil) |
| | var got string |
| | if err != nil { |
| | got = err.Error() |
| | } |
| | if test.err == "" { |
| | if got != "" { |
| | t.Errorf("input=%q: unexpected error %q", test.input, got) |
| | } |
| | continue |
| | } |
| | if !strings.Contains(got, test.err) { |
| | t.Errorf("input=%q: error\n\t%q\ndoes not contain expected string\n\t%q", test.input, got, test.err) |
| | continue |
| | } |
| | |
| | if err := tmpl.Execute(buf, nil); err == nil || err.Error() != got { |
| | t.Errorf("input=%q: unexpected error on second call %q", test.input, err) |
| |
|
| | } |
| | } |
| | } |
| |
|
| | func TestEscapeText(t *testing.T) { |
| | tests := []struct { |
| | input string |
| | output context |
| | }{ |
| | { |
| | ``, |
| | context{}, |
| | }, |
| | { |
| | `Hello, World!`, |
| | context{}, |
| | }, |
| | { |
| | |
| | `I <3 Ponies!`, |
| | context{}, |
| | }, |
| | { |
| | `<a`, |
| | context{state: stateTag}, |
| | }, |
| | { |
| | `<a `, |
| | context{state: stateTag}, |
| | }, |
| | { |
| | `<a>`, |
| | context{state: stateText}, |
| | }, |
| | { |
| | `<a href`, |
| | context{state: stateAttrName, attr: attrURL}, |
| | }, |
| | { |
| | `<a on`, |
| | context{state: stateAttrName, attr: attrScript}, |
| | }, |
| | { |
| | `<a href `, |
| | context{state: stateAfterName, attr: attrURL}, |
| | }, |
| | { |
| | `<a style = `, |
| | context{state: stateBeforeValue, attr: attrStyle}, |
| | }, |
| | { |
| | `<a href=`, |
| | context{state: stateBeforeValue, attr: attrURL}, |
| | }, |
| | { |
| | `<a href=x`, |
| | context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery, attr: attrURL}, |
| | }, |
| | { |
| | `<a href=x `, |
| | context{state: stateTag}, |
| | }, |
| | { |
| | `<a href=>`, |
| | context{state: stateText}, |
| | }, |
| | { |
| | `<a href=x>`, |
| | context{state: stateText}, |
| | }, |
| | { |
| | `<a href ='`, |
| | context{state: stateURL, delim: delimSingleQuote, attr: attrURL}, |
| | }, |
| | { |
| | `<a href=''`, |
| | context{state: stateTag}, |
| | }, |
| | { |
| | `<a href= "`, |
| | context{state: stateURL, delim: delimDoubleQuote, attr: attrURL}, |
| | }, |
| | { |
| | `<a href=""`, |
| | context{state: stateTag}, |
| | }, |
| | { |
| | `<a title="`, |
| | context{state: stateAttr, delim: delimDoubleQuote}, |
| | }, |
| | { |
| | `<a HREF='http:`, |
| | context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL}, |
| | }, |
| | { |
| | `<a Href='/`, |
| | context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL}, |
| | }, |
| | { |
| | `<a href='"`, |
| | context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL}, |
| | }, |
| | { |
| | `<a href="'`, |
| | context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrURL}, |
| | }, |
| | { |
| | `<a href=''`, |
| | context{state: stateURL, delim: delimSingleQuote, urlPart: urlPartPreQuery, attr: attrURL}, |
| | }, |
| | { |
| | `<a href=""`, |
| | context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrURL}, |
| | }, |
| | { |
| | `<a href=""`, |
| | context{state: stateURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrURL}, |
| | }, |
| | { |
| | `<a href="`, |
| | context{state: stateURL, delim: delimSpaceOrTagEnd, urlPart: urlPartPreQuery, attr: attrURL}, |
| | }, |
| | { |
| | `<img alt="1">`, |
| | context{state: stateText}, |
| | }, |
| | { |
| | `<img alt="1>"`, |
| | context{state: stateTag}, |
| | }, |
| | { |
| | `<img alt="1>">`, |
| | context{state: stateText}, |
| | }, |
| | { |
| | `<input checked type="checkbox"`, |
| | context{state: stateTag}, |
| | }, |
| | { |
| | `<a onclick="`, |
| | context{state: stateJS, delim: delimDoubleQuote, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick="//foo`, |
| | context{state: stateJSLineCmt, delim: delimDoubleQuote, attr: attrScript}, |
| | }, |
| | { |
| | "<a onclick='//\n", |
| | context{state: stateJS, delim: delimSingleQuote, attr: attrScript}, |
| | }, |
| | { |
| | "<a onclick='//\r\n", |
| | context{state: stateJS, delim: delimSingleQuote, attr: attrScript}, |
| | }, |
| | { |
| | "<a onclick='//\u2028", |
| | context{state: stateJS, delim: delimSingleQuote, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick="/*`, |
| | context{state: stateJSBlockCmt, delim: delimDoubleQuote, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick="/*/`, |
| | context{state: stateJSBlockCmt, delim: delimDoubleQuote, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick="/**/`, |
| | context{state: stateJS, delim: delimDoubleQuote, attr: attrScript}, |
| | }, |
| | { |
| | `<a onkeypress=""`, |
| | context{state: stateJSDqStr, delim: delimDoubleQuote, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick='"foo"`, |
| | context{state: stateJS, delim: delimSingleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick='foo'`, |
| | context{state: stateJS, delim: delimSpaceOrTagEnd, jsCtx: jsCtxDivOp, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick='foo`, |
| | context{state: stateJSSqStr, delim: delimSpaceOrTagEnd, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick=""foo'`, |
| | context{state: stateJSDqStr, delim: delimDoubleQuote, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick="'foo"`, |
| | context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, |
| | }, |
| | { |
| | "<a onclick=\"`foo", |
| | context{state: stateJSTmplLit, delim: delimDoubleQuote, attr: attrScript}, |
| | }, |
| | { |
| | `<A ONCLICK="'`, |
| | context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick="/`, |
| | context{state: stateJSRegexp, delim: delimDoubleQuote, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick="'foo'`, |
| | context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick="'foo\'`, |
| | context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick="'foo\'`, |
| | context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick="/foo/`, |
| | context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, |
| | }, |
| | { |
| | `<script>/foo/ /=`, |
| | context{state: stateJS, element: elementScript}, |
| | }, |
| | { |
| | `<a onclick="1 /foo`, |
| | context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick="1 /*c*/ /foo`, |
| | context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick="/foo[/]`, |
| | context{state: stateJSRegexp, delim: delimDoubleQuote, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick="/foo\/`, |
| | context{state: stateJSRegexp, delim: delimDoubleQuote, attr: attrScript}, |
| | }, |
| | { |
| | `<a onclick="/foo/`, |
| | context{state: stateJS, delim: delimDoubleQuote, jsCtx: jsCtxDivOp, attr: attrScript}, |
| | }, |
| | { |
| | `<input checked style="`, |
| | context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="//`, |
| | context{state: stateCSSLineCmt, delim: delimDoubleQuote, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="//</script>`, |
| | context{state: stateCSSLineCmt, delim: delimDoubleQuote, attr: attrStyle}, |
| | }, |
| | { |
| | "<a style='//\n", |
| | context{state: stateCSS, delim: delimSingleQuote, attr: attrStyle}, |
| | }, |
| | { |
| | "<a style='//\r", |
| | context{state: stateCSS, delim: delimSingleQuote, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="/*`, |
| | context{state: stateCSSBlockCmt, delim: delimDoubleQuote, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="/*/`, |
| | context{state: stateCSSBlockCmt, delim: delimDoubleQuote, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="/**/`, |
| | context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="background: '`, |
| | context{state: stateCSSSqStr, delim: delimDoubleQuote, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="background: "`, |
| | context{state: stateCSSDqStr, delim: delimDoubleQuote, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="background: '/foo?img=`, |
| | context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="background: '/`, |
| | context{state: stateCSSSqStr, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="background: url("/`, |
| | context{state: stateCSSDqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="background: url('/`, |
| | context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="background: url('/)`, |
| | context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="background: url('/ `, |
| | context{state: stateCSSSqURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="background: url(/`, |
| | context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartPreQuery, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="background: url( `, |
| | context{state: stateCSSURL, delim: delimDoubleQuote, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="background: url( /image?name=`, |
| | context{state: stateCSSURL, delim: delimDoubleQuote, urlPart: urlPartQueryOrFrag, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="background: url(x)`, |
| | context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="background: url('x'`, |
| | context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, |
| | }, |
| | { |
| | `<a style="background: url( x `, |
| | context{state: stateCSS, delim: delimDoubleQuote, attr: attrStyle}, |
| | }, |
| | { |
| | `<!-- foo`, |
| | context{state: stateHTMLCmt}, |
| | }, |
| | { |
| | `<!-->`, |
| | context{state: stateHTMLCmt}, |
| | }, |
| | { |
| | `<!--->`, |
| | context{state: stateHTMLCmt}, |
| | }, |
| | { |
| | `<!-- foo -->`, |
| | context{state: stateText}, |
| | }, |
| | { |
| | `<script`, |
| | context{state: stateTag, element: elementScript}, |
| | }, |
| | { |
| | `<script `, |
| | context{state: stateTag, element: elementScript}, |
| | }, |
| | { |
| | `<script src="foo.js" `, |
| | context{state: stateTag, element: elementScript}, |
| | }, |
| | { |
| | `<script src='foo.js' `, |
| | context{state: stateTag, element: elementScript}, |
| | }, |
| | { |
| | `<script type=text/javascript `, |
| | context{state: stateTag, element: elementScript}, |
| | }, |
| | { |
| | `<script>`, |
| | context{state: stateJS, jsCtx: jsCtxRegexp, element: elementScript}, |
| | }, |
| | { |
| | `<script>foo`, |
| | context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, |
| | }, |
| | { |
| | `<script>foo</script>`, |
| | context{state: stateText}, |
| | }, |
| | { |
| | `<script>foo</script><!--`, |
| | context{state: stateHTMLCmt}, |
| | }, |
| | { |
| | `<script>document.write("<p>foo</p>");`, |
| | context{state: stateJS, element: elementScript}, |
| | }, |
| | { |
| | `<script>document.write("<p>foo<\/script>");`, |
| | context{state: stateJS, element: elementScript}, |
| | }, |
| | { |
| | |
| | |
| | `<script>document.write("<script>alert(1)</script>");`, |
| | context{state: stateJS, element: elementScript}, |
| | }, |
| | { |
| | `<script>document.write("<script>`, |
| | context{state: stateJSDqStr, element: elementScript}, |
| | }, |
| | { |
| | `<script>document.write("<script>alert(1)</script>`, |
| | context{state: stateJSDqStr, element: elementScript}, |
| | }, |
| | { |
| | `<script>document.write("<script>alert(1)<!--`, |
| | context{state: stateJSDqStr, element: elementScript}, |
| | }, |
| | { |
| | `<script>document.write("<script>alert(1)</Script>");`, |
| | context{state: stateJS, element: elementScript}, |
| | }, |
| | { |
| | `<script>document.write("<!--");`, |
| | context{state: stateJS, element: elementScript}, |
| | }, |
| | { |
| | `<script>let a = /</script`, |
| | context{state: stateJSRegexp, element: elementScript}, |
| | }, |
| | { |
| | `<script>let a = /</script/`, |
| | context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp}, |
| | }, |
| | { |
| | `<script type="text/template">`, |
| | context{state: stateText}, |
| | }, |
| | |
| | { |
| | `<script type="TEXT/JAVASCRIPT">`, |
| | context{state: stateJS, element: elementScript}, |
| | }, |
| | |
| | { |
| | `<script TYPE="text/template">`, |
| | context{state: stateText}, |
| | }, |
| | { |
| | `<script type="notjs">`, |
| | context{state: stateText}, |
| | }, |
| | { |
| | `<Script>`, |
| | context{state: stateJS, element: elementScript}, |
| | }, |
| | { |
| | `<SCRIPT>foo`, |
| | context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, |
| | }, |
| | { |
| | `<textarea>value`, |
| | context{state: stateRCDATA, element: elementTextarea}, |
| | }, |
| | { |
| | `<textarea>value</TEXTAREA>`, |
| | context{state: stateText}, |
| | }, |
| | { |
| | `<textarea name=html><b`, |
| | context{state: stateRCDATA, element: elementTextarea}, |
| | }, |
| | { |
| | `<title>value`, |
| | context{state: stateRCDATA, element: elementTitle}, |
| | }, |
| | { |
| | `<style>value`, |
| | context{state: stateCSS, element: elementStyle}, |
| | }, |
| | { |
| | `<a xlink:href`, |
| | context{state: stateAttrName, attr: attrURL}, |
| | }, |
| | { |
| | `<a xmlns`, |
| | context{state: stateAttrName, attr: attrURL}, |
| | }, |
| | { |
| | `<a xmlns:foo`, |
| | context{state: stateAttrName, attr: attrURL}, |
| | }, |
| | { |
| | `<a xmlnsxyz`, |
| | context{state: stateAttrName}, |
| | }, |
| | { |
| | `<a data-url`, |
| | context{state: stateAttrName, attr: attrURL}, |
| | }, |
| | { |
| | `<a data-iconUri`, |
| | context{state: stateAttrName, attr: attrURL}, |
| | }, |
| | { |
| | `<a data-urlItem`, |
| | context{state: stateAttrName, attr: attrURL}, |
| | }, |
| | { |
| | `<a g:`, |
| | context{state: stateAttrName}, |
| | }, |
| | { |
| | `<a g:url`, |
| | context{state: stateAttrName, attr: attrURL}, |
| | }, |
| | { |
| | `<a g:iconUri`, |
| | context{state: stateAttrName, attr: attrURL}, |
| | }, |
| | { |
| | `<a g:urlItem`, |
| | context{state: stateAttrName, attr: attrURL}, |
| | }, |
| | { |
| | `<a g:value`, |
| | context{state: stateAttrName}, |
| | }, |
| | { |
| | `<a svg:style='`, |
| | context{state: stateCSS, delim: delimSingleQuote, attr: attrStyle}, |
| | }, |
| | { |
| | `<svg:font-face`, |
| | context{state: stateTag}, |
| | }, |
| | { |
| | `<svg:a svg:onclick="`, |
| | context{state: stateJS, delim: delimDoubleQuote, attr: attrScript}, |
| | }, |
| | { |
| | `<svg:a svg:onclick="x()">`, |
| | context{}, |
| | }, |
| | { |
| | "<script>var a = `", |
| | context{state: stateJSTmplLit, element: elementScript}, |
| | }, |
| | { |
| | "<script>var a = `${", |
| | context{state: stateJS, element: elementScript}, |
| | }, |
| | { |
| | "<script>var a = `${}", |
| | context{state: stateJSTmplLit, element: elementScript}, |
| | }, |
| | { |
| | "<script>var a = `${`", |
| | context{state: stateJSTmplLit, element: elementScript}, |
| | }, |
| | { |
| | "<script>var a = `${var a = \"", |
| | context{state: stateJSDqStr, element: elementScript}, |
| | }, |
| | { |
| | "<script>var a = `${var a = \"`", |
| | context{state: stateJSDqStr, element: elementScript}, |
| | }, |
| | { |
| | "<script>var a = `${var a = \"}", |
| | context{state: stateJSDqStr, element: elementScript}, |
| | }, |
| | { |
| | "<script>var a = `${``", |
| | context{state: stateJS, element: elementScript}, |
| | }, |
| | { |
| | "<script>var a = `${`}", |
| | context{state: stateJSTmplLit, element: elementScript}, |
| | }, |
| | { |
| | "<script>`${ {} } asd`</script><script>`${ {} }", |
| | context{state: stateJSTmplLit, element: elementScript}, |
| | }, |
| | { |
| | "<script>var foo = `${ (_ => { return \"x\" })() + \"${", |
| | context{state: stateJSDqStr, element: elementScript}, |
| | }, |
| | { |
| | "<script>var a = `${ {</script><script>var b = `${ x }", |
| | context{state: stateJSTmplLit, element: elementScript, jsCtx: jsCtxDivOp}, |
| | }, |
| | { |
| | "<script>var foo = `x` + \"${", |
| | context{state: stateJSDqStr, element: elementScript}, |
| | }, |
| | { |
| | "<script>function f() { var a = `${}`; }", |
| | context{state: stateJS, element: elementScript}, |
| | }, |
| | { |
| | "<script>{`${}`}", |
| | context{state: stateJS, element: elementScript}, |
| | }, |
| | { |
| | "<script>`${ function f() { return `${1}` }() }`", |
| | context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp}, |
| | }, |
| | { |
| | "<script>function f() {`${ function f() { `${1}` } }`}", |
| | context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp}, |
| | }, |
| | { |
| | "<script>`${ { `` }", |
| | context{state: stateJS, element: elementScript}, |
| | }, |
| | { |
| | "<script>`${ { }`", |
| | context{state: stateJSTmplLit, element: elementScript}, |
| | }, |
| | { |
| | "<script>var foo = `${ foo({ a: { c: `${", |
| | context{state: stateJS, element: elementScript}, |
| | }, |
| | { |
| | "<script>var foo = `${ foo({ a: { c: `${ {{.}} }` }, b: ", |
| | context{state: stateJS, element: elementScript}, |
| | }, |
| | { |
| | "<script>`${ `}", |
| | context{state: stateJSTmplLit, element: elementScript}, |
| | }, |
| | } |
| |
|
| | for _, test := range tests { |
| | b, e := []byte(test.input), makeEscaper(nil) |
| | c := e.escapeText(context{}, &parse.TextNode{NodeType: parse.NodeText, Text: b}) |
| | if !test.output.eq(c) { |
| | t.Errorf("input %q: want context\n\t%v\ngot\n\t%v", test.input, test.output, c) |
| | continue |
| | } |
| | if test.input != string(b) { |
| | t.Errorf("input %q: text node was modified: want %q got %q", test.input, test.input, b) |
| | continue |
| | } |
| | } |
| | } |
| |
|
| | func TestEnsurePipelineContains(t *testing.T) { |
| | tests := []struct { |
| | input, output string |
| | ids []string |
| | }{ |
| | { |
| | "{{.X}}", |
| | ".X", |
| | []string{}, |
| | }, |
| | { |
| | "{{.X | html}}", |
| | ".X | html", |
| | []string{}, |
| | }, |
| | { |
| | "{{.X}}", |
| | ".X | html", |
| | []string{"html"}, |
| | }, |
| | { |
| | "{{html .X}}", |
| | "_eval_args_ .X | html | urlquery", |
| | []string{"html", "urlquery"}, |
| | }, |
| | { |
| | "{{html .X .Y .Z}}", |
| | "_eval_args_ .X .Y .Z | html | urlquery", |
| | []string{"html", "urlquery"}, |
| | }, |
| | { |
| | "{{.X | print}}", |
| | ".X | print | urlquery", |
| | []string{"urlquery"}, |
| | }, |
| | { |
| | "{{.X | print | urlquery}}", |
| | ".X | print | urlquery", |
| | []string{"urlquery"}, |
| | }, |
| | { |
| | "{{.X | urlquery}}", |
| | ".X | html | urlquery", |
| | []string{"html", "urlquery"}, |
| | }, |
| | { |
| | "{{.X | print 2 | .f 3}}", |
| | ".X | print 2 | .f 3 | urlquery | html", |
| | []string{"urlquery", "html"}, |
| | }, |
| | { |
| | |
| | "{{.X | println.x }}", |
| | ".X | println.x | urlquery | html", |
| | []string{"urlquery", "html"}, |
| | }, |
| | { |
| | |
| | "{{.X | (print 12 | println).x }}", |
| | ".X | (print 12 | println).x | urlquery | html", |
| | []string{"urlquery", "html"}, |
| | }, |
| | |
| | |
| | { |
| | "{{.X | urlquery}}", |
| | ".X | _html_template_urlfilter | urlquery", |
| | []string{"_html_template_urlfilter", "_html_template_urlnormalizer"}, |
| | }, |
| | { |
| | "{{.X | urlquery}}", |
| | ".X | urlquery | _html_template_urlfilter | _html_template_cssescaper", |
| | []string{"_html_template_urlfilter", "_html_template_cssescaper"}, |
| | }, |
| | { |
| | "{{.X | urlquery}}", |
| | ".X | urlquery", |
| | []string{"_html_template_urlnormalizer"}, |
| | }, |
| | { |
| | "{{.X | urlquery}}", |
| | ".X | urlquery", |
| | []string{"_html_template_urlescaper"}, |
| | }, |
| | { |
| | "{{.X | html}}", |
| | ".X | html", |
| | []string{"_html_template_htmlescaper"}, |
| | }, |
| | { |
| | "{{.X | html}}", |
| | ".X | html", |
| | []string{"_html_template_rcdataescaper"}, |
| | }, |
| | } |
| | for i, test := range tests { |
| | tmpl := template.Must(template.New("test").Parse(test.input)) |
| | action, ok := (tmpl.Tree.Root.Nodes[0].(*parse.ActionNode)) |
| | if !ok { |
| | t.Errorf("First node is not an action: %s", test.input) |
| | continue |
| | } |
| | pipe := action.Pipe |
| | originalIDs := make([]string, len(test.ids)) |
| | copy(originalIDs, test.ids) |
| | ensurePipelineContains(pipe, test.ids) |
| | got := pipe.String() |
| | if got != test.output { |
| | t.Errorf("#%d: %s, %v: want\n\t%s\ngot\n\t%s", i, test.input, originalIDs, test.output, got) |
| | } |
| | } |
| | } |
| |
|
| | func TestEscapeMalformedPipelines(t *testing.T) { |
| | tests := []string{ |
| | "{{ 0 | $ }}", |
| | "{{ 0 | $ | urlquery }}", |
| | "{{ 0 | (nil) }}", |
| | "{{ 0 | (nil) | html }}", |
| | } |
| | for _, test := range tests { |
| | var b bytes.Buffer |
| | tmpl, err := New("test").Parse(test) |
| | if err != nil { |
| | t.Errorf("failed to parse set: %q", err) |
| | } |
| | err = tmpl.Execute(&b, nil) |
| | if err == nil { |
| | t.Errorf("Expected error for %q", test) |
| | } |
| | } |
| | } |
| |
|
| | func TestEscapeErrorsNotIgnorable(t *testing.T) { |
| | var b bytes.Buffer |
| | tmpl, _ := New("dangerous").Parse("<a") |
| | err := tmpl.Execute(&b, nil) |
| | if err == nil { |
| | t.Errorf("Expected error") |
| | } else if b.Len() != 0 { |
| | t.Errorf("Emitted output despite escaping failure") |
| | } |
| | } |
| |
|
| | func TestEscapeSetErrorsNotIgnorable(t *testing.T) { |
| | var b bytes.Buffer |
| | tmpl, err := New("root").Parse(`{{define "t"}}<a{{end}}`) |
| | if err != nil { |
| | t.Errorf("failed to parse set: %q", err) |
| | } |
| | err = tmpl.ExecuteTemplate(&b, "t", nil) |
| | if err == nil { |
| | t.Errorf("Expected error") |
| | } else if b.Len() != 0 { |
| | t.Errorf("Emitted output despite escaping failure") |
| | } |
| | } |
| |
|
| | func TestRedundantFuncs(t *testing.T) { |
| | inputs := []any{ |
| | "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + |
| | "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + |
| | ` !"#$%&'()*+,-./` + |
| | `0123456789:;<=>?` + |
| | `@ABCDEFGHIJKLMNO` + |
| | `PQRSTUVWXYZ[\]^_` + |
| | "`abcdefghijklmno" + |
| | "pqrstuvwxyz{|}~\x7f" + |
| | "\u00A0\u0100\u2028\u2029\ufeff\ufdec\ufffd\uffff\U0001D11E" + |
| | "&%22\\", |
| | CSS(`a[href =~ "//example.com"]#foo`), |
| | HTML(`Hello, <b>World</b> &tc!`), |
| | HTMLAttr(` dir="ltr"`), |
| | JS(`c && alert("Hello, World!");`), |
| | JSStr(`Hello, World & O'Reilly\x21`), |
| | URL(`greeting=H%69&addressee=(World)`), |
| | } |
| |
|
| | for n0, m := range redundantFuncs { |
| | f0 := funcMap[n0].(func(...any) string) |
| | for n1 := range m { |
| | f1 := funcMap[n1].(func(...any) string) |
| | for _, input := range inputs { |
| | want := f0(input) |
| | if got := f1(want); want != got { |
| | t.Errorf("%s %s with %T %q: want\n\t%q,\ngot\n\t%q", n0, n1, input, input, want, got) |
| | } |
| | } |
| | } |
| | } |
| | } |
| |
|
| | func TestIndirectPrint(t *testing.T) { |
| | a := 3 |
| | ap := &a |
| | b := "hello" |
| | bp := &b |
| | bpp := &bp |
| | tmpl := Must(New("t").Parse(`{{.}}`)) |
| | var buf strings.Builder |
| | err := tmpl.Execute(&buf, ap) |
| | if err != nil { |
| | t.Errorf("Unexpected error: %s", err) |
| | } else if buf.String() != "3" { |
| | t.Errorf(`Expected "3"; got %q`, buf.String()) |
| | } |
| | buf.Reset() |
| | err = tmpl.Execute(&buf, bpp) |
| | if err != nil { |
| | t.Errorf("Unexpected error: %s", err) |
| | } else if buf.String() != "hello" { |
| | t.Errorf(`Expected "hello"; got %q`, buf.String()) |
| | } |
| | } |
| |
|
| | |
| | func TestEmptyTemplateHTML(t *testing.T) { |
| | page := Must(New("page").ParseFiles(os.DevNull)) |
| | if err := page.ExecuteTemplate(os.Stdout, "page", "nothing"); err == nil { |
| | t.Fatal("expected error") |
| | } |
| | } |
| |
|
| | type Issue7379 int |
| |
|
| | func (Issue7379) SomeMethod(x int) string { |
| | return fmt.Sprintf("<%d>", x) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | func TestPipeToMethodIsEscaped(t *testing.T) { |
| | tmpl := Must(New("x").Parse("<html>{{0 | .SomeMethod}}</html>\n")) |
| | tryExec := func() string { |
| | defer func() { |
| | panicValue := recover() |
| | if panicValue != nil { |
| | t.Errorf("panicked: %v\n", panicValue) |
| | } |
| | }() |
| | var b strings.Builder |
| | tmpl.Execute(&b, Issue7379(0)) |
| | return b.String() |
| | } |
| | for i := 0; i < 3; i++ { |
| | str := tryExec() |
| | const expect = "<html><0></html>\n" |
| | if str != expect { |
| | t.Errorf("expected %q got %q", expect, str) |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | func TestErrorOnUndefined(t *testing.T) { |
| | tmpl := New("undefined") |
| |
|
| | err := tmpl.Execute(nil, nil) |
| | if err == nil { |
| | t.Error("expected error") |
| | } else if !strings.Contains(err.Error(), "incomplete") { |
| | t.Errorf("expected error about incomplete template; got %s", err) |
| | } |
| | } |
| |
|
| | |
| | func TestIdempotentExecute(t *testing.T) { |
| | tmpl := Must(New(""). |
| | Parse(`{{define "main"}}<body>{{template "hello"}}</body>{{end}}`)) |
| | Must(tmpl. |
| | Parse(`{{define "hello"}}Hello, {{"Ladies & Gentlemen!"}}{{end}}`)) |
| | got := new(strings.Builder) |
| | var err error |
| | |
| | want := "Hello, Ladies & Gentlemen!" |
| | for i := 0; i < 2; i++ { |
| | err = tmpl.ExecuteTemplate(got, "hello", nil) |
| | if err != nil { |
| | t.Errorf("unexpected error: %s", err) |
| | } |
| | if got.String() != want { |
| | t.Errorf("after executing template \"hello\", got:\n\t%q\nwant:\n\t%q\n", got.String(), want) |
| | } |
| | got.Reset() |
| | } |
| | |
| | |
| | err = tmpl.ExecuteTemplate(got, "main", nil) |
| | if err != nil { |
| | t.Errorf("unexpected error: %s", err) |
| | } |
| | |
| | |
| | want = "<body>Hello, Ladies & Gentlemen!</body>" |
| | if got.String() != want { |
| | t.Errorf("after executing template \"main\", got:\n\t%q\nwant:\n\t%q\n", got.String(), want) |
| | } |
| | } |
| |
|
| | func BenchmarkEscapedExecute(b *testing.B) { |
| | tmpl := Must(New("t").Parse(`<a onclick="alert('{{.}}')">{{.}}</a>`)) |
| | var buf bytes.Buffer |
| | b.ResetTimer() |
| | for i := 0; i < b.N; i++ { |
| | tmpl.Execute(&buf, "foo & 'bar' & baz") |
| | buf.Reset() |
| | } |
| | } |
| |
|
| | |
| | func TestOrphanedTemplate(t *testing.T) { |
| | t1 := Must(New("foo").Parse(`<a href="{{.}}">link1</a>`)) |
| | t2 := Must(t1.New("foo").Parse(`bar`)) |
| |
|
| | var b strings.Builder |
| | const wantError = `template: "foo" is an incomplete or empty template` |
| | if err := t1.Execute(&b, "javascript:alert(1)"); err == nil { |
| | t.Fatal("expected error executing t1") |
| | } else if gotError := err.Error(); gotError != wantError { |
| | t.Fatalf("got t1 execution error:\n\t%s\nwant:\n\t%s", gotError, wantError) |
| | } |
| | b.Reset() |
| | if err := t2.Execute(&b, nil); err != nil { |
| | t.Fatalf("error executing t2: %s", err) |
| | } |
| | const want = "bar" |
| | if got := b.String(); got != want { |
| | t.Fatalf("t2 rendered %q, want %q", got, want) |
| | } |
| | } |
| |
|
| | |
| | func TestAliasedParseTreeDoesNotOverescape(t *testing.T) { |
| | const ( |
| | tmplText = `{{.}}` |
| | data = `<baz>` |
| | want = `<baz>` |
| | ) |
| | |
| | tpl := Must(New("foo").Parse(tmplText)) |
| | if _, err := tpl.AddParseTree("bar", tpl.Tree); err != nil { |
| | t.Fatalf("AddParseTree error: %v", err) |
| | } |
| | var b1, b2 strings.Builder |
| | if err := tpl.ExecuteTemplate(&b1, "foo", data); err != nil { |
| | t.Fatalf(`ExecuteTemplate failed for "foo": %v`, err) |
| | } |
| | if err := tpl.ExecuteTemplate(&b2, "bar", data); err != nil { |
| | t.Fatalf(`ExecuteTemplate failed for "foo": %v`, err) |
| | } |
| | got1, got2 := b1.String(), b2.String() |
| | if got1 != want { |
| | t.Fatalf(`Template "foo" rendered %q, want %q`, got1, want) |
| | } |
| | if got1 != got2 { |
| | t.Fatalf(`Template "foo" and "bar" rendered %q and %q respectively, expected equal values`, got1, got2) |
| | } |
| | } |
| |
|