func TestEscape(t *testing.T) { data := struct { F, T bool C, G, H string A, E []string B, M json.Marshaler N int Z *int W escape.HTML }{ F: false, T: true, C: "<Cincinatti>", G: "<Goodbye>", H: "<Hello>", A: []string{"<a>", "<b>"}, E: []string{}, N: 42, B: &badMarshaler{}, M: &goodMarshaler{}, Z: nil, W: escape.HTML(`¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`), } pdata := &data tests := []struct { name string input string output string }{ { "if", "{{if .T}}Hello{{end}}, {{.C}}!", "Hello, <Cincinatti>!", }, { "else", "{{if .F}}{{.H}}{{else}}{{.G}}{{end}}!", "<Goodbye>!", }, { "overescaping1", "Hello, {{.C | html}}!", "Hello, <Cincinatti>!", }, { "overescaping2", "Hello, {{html .C}}!", "Hello, <Cincinatti>!", }, { "overescaping3", "{{with .C}}{{$msg := .}}Hello, {{$msg}}!{{end}}", "Hello, <Cincinatti>!", }, { "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", }, { "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 <*****@*****.**>"}}>`, `<a href=mailto:Muhammed%20%22The%20Greatest%22%20Ali%20%[email protected]%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=%3cCincinatti%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 )'>`, }, { "jsNilValue", "<button onclick='alert(typeof{{.Z}})'>", `<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("\x3cHello\x3e")'>`, }, { "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}})'>", // URL escaped, then quoted for JS. `<button onclick='alert("%3CCincinatti%3E")'>`, }, { "jsRe", `<button onclick='alert(/{{"foo+bar"}}/.test(""))'>`, `<button onclick='alert(/foo\x2bbar/.test(""))'>`, }, { "jsReBlank", `<script>alert(/{{""}}/.test(""));</script>`, `<script>alert(/(?:)/.test(""));</script>`, }, { "jsReAmbigOk", `<script>{{if true}}var x = 1{{end}}</script>`, // The {if} ends in an ambiguous jsCtx but there is // no slash following so we shouldn't care. `<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)"}}'">`, // The CSS string 'javascript\\3a alert(1337)' does not contains a colon. `<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, <Cincinatti></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, <Cincinatti></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>", // Newline separates break from call. If newline // removed, then break will consume label leaving // code invalid. "<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>", // Newline separates break from call. If newline // removed, then break will consume label leaving // code invalid. "<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>", }, { "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}}) */">`, // Attribute comment tests should pass if the comments // are successfully elided. `<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("&iexcl;\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>", }, { "auditable exemption from escaping", "{{range .A}}{{. | noescape}}{{end}}", "<a><b>", }, { "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}}` + // Double quotes inside if/else. ` src=` + `{{if .T}}"?{{"<iconPath>"}}"` + `{{else}}"images/cleardot.gif"{{end}}` + // Missing space before title, but it is not a // part of the src attribute. `{{if .T}}title="{{"<title>"}}"{{end}}` + // Quotes outside if/else. ` 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"}})">`, // Treated as JS since quotes are inserted. `<img onload="alert("loaded")">`, }, { "bad dynamic attribute name 1", // Allow checked, selected, disabled, but not JS or // CSS attributes. `<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", // Allow title or alt, but not a URL. `<img {{"src"}}="{{"javascript:doEvil()"}}">`, `<img ZgotmplZ="javascript:doEvil()">`, }, { "bad dynamic attribute name 4", // Structure preservation requires values to associate // with a consistent attribute. `<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", // Dynamic element names are typically used to switch // between (thead, tfoot, tbody), (ul, ol), (th, td), // and other replaceable sets. // We do not currently easily support (ul, ol). // If we do change to support that, this test should // catch failures to filter out special tag names which // would violate the structure preservation property -- // if any special tag name could be substituted, then // the content could be raw text/RCDATA for some inputs // and regular HTML content for others. `<{{"script"}}>{{"doEvil()"}}</{{"script"}}>`, `<script>doEvil()</script>`, }, } for _, test := range tests { tmpl := new(Set) // TODO: Move noescape into template/func.go tmpl.Funcs(FuncMap{ "noescape": func(a ...interface{}) string { return fmt.Sprint(a...) }, }) text := fmt.Sprintf(`{{define %q}}%s{{end}}`, test.name, test.input) tmpl = Must(tmpl.Parse(text)).Escape() b := new(bytes.Buffer) if err := tmpl.Execute(b, test.name, data); err != nil { t.Errorf("%s: template execution failed: %s", test.name, 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.name, w, g) continue } b.Reset() if err := tmpl.Execute(b, test.name, pdata); err != nil { t.Errorf("%s: template execution failed for pointer: %s", test.name, err) continue } if w, g := test.output, b.String(); w != g { t.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g) continue } } }
func TestTypedContent(t *testing.T) { data := []interface{}{ `<b> "foo%" O'Reilly &bar;`, escape.CSS(`a[href =~ "//example.com"]#foo`), escape.HTML(`Hello, <b>World</b> &tc!`), escape.HTMLAttr(` dir="ltr"`), escape.JS(`c && alert("Hello, World!");`), escape.JSStr(`Hello, World & O'Reilly\x21`), escape.URL(`greeting=H%69&addressee=(World)`), } // For each content sensitive escaper, see how it does on // each of the typed strings above. tests := []struct { // A template containing a single {{.}}. input string want []string }{ { `<style>{{.}} { color: blue }</style>`, []string{ `ZgotmplZ`, // Allowed but not escaped. `a[href =~ "//example.com"]#foo`, `ZgotmplZ`, `ZgotmplZ`, `ZgotmplZ`, `ZgotmplZ`, `ZgotmplZ`, }, }, { `<div style="{{.}}">`, []string{ `ZgotmplZ`, // Allowed and HTML escaped. `a[href =~ "//example.com"]#foo`, `ZgotmplZ`, `ZgotmplZ`, `ZgotmplZ`, `ZgotmplZ`, `ZgotmplZ`, }, }, { `{{.}}`, []string{ `<b> "foo%" O'Reilly &bar;`, `a[href =~ "//example.com"]#foo`, // Not escaped. `Hello, <b>World</b> &tc!`, ` dir="ltr"`, `c && alert("Hello, World!");`, `Hello, World & O'Reilly\x21`, `greeting=H%69&addressee=(World)`, }, }, { `<a{{.}}>`, []string{ `ZgotmplZ`, `ZgotmplZ`, `ZgotmplZ`, // Allowed and HTML escaped. ` dir="ltr"`, `ZgotmplZ`, `ZgotmplZ`, `ZgotmplZ`, }, }, { `<a title={{.}}>`, []string{ `<b> "foo%" O'Reilly &bar;`, `a[href =~ "//example.com"]#foo`, // Tags stripped, spaces escaped, entity not re-escaped. `Hello, World &tc!`, ` dir="ltr"`, `c && alert("Hello, World!");`, `Hello, World & O'Reilly\x21`, `greeting=H%69&addressee=(World)`, }, }, { `<a title='{{.}}'>`, []string{ `<b> "foo%" O'Reilly &bar;`, `a[href =~ "//example.com"]#foo`, // Tags stripped, entity not re-escaped. `Hello, World &tc!`, ` dir="ltr"`, `c && alert("Hello, World!");`, `Hello, World & O'Reilly\x21`, `greeting=H%69&addressee=(World)`, }, }, { `<textarea>{{.}}</textarea>`, []string{ `<b> "foo%" O'Reilly &bar;`, `a[href =~ "//example.com"]#foo`, // Angle brackets escaped to prevent injection of close tags, entity not re-escaped. `Hello, <b>World</b> &tc!`, ` dir="ltr"`, `c && alert("Hello, World!");`, `Hello, World & O'Reilly\x21`, `greeting=H%69&addressee=(World)`, }, }, { `<script>alert({{.}})</script>`, []string{ `"\u003cb\u003e \"foo%\" O'Reilly &bar;"`, `"a[href =~ \"//example.com\"]#foo"`, `"Hello, \u003cb\u003eWorld\u003c/b\u003e &tc!"`, `" dir=\"ltr\""`, // Not escaped. `c && alert("Hello, World!");`, // Escape sequence not over-escaped. `"Hello, World & O'Reilly\x21"`, `"greeting=H%69&addressee=(World)"`, }, }, { `<button onclick="alert({{.}})">`, []string{ `"\u003cb\u003e \"foo%\" O'Reilly &bar;"`, `"a[href =~ \"//example.com\"]#foo"`, `"Hello, \u003cb\u003eWorld\u003c/b\u003e &amp;tc!"`, `" dir=\"ltr\""`, // Not JS escaped but HTML escaped. `c && alert("Hello, World!");`, // Escape sequence not over-escaped. `"Hello, World & O'Reilly\x21"`, `"greeting=H%69&addressee=(World)"`, }, }, { `<script>alert("{{.}}")</script>`, []string{ `\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`, `a[href =~ \x22\/\/example.com\x22]#foo`, `Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`, ` dir=\x22ltr\x22`, `c \x26\x26 alert(\x22Hello, World!\x22);`, // Escape sequence not over-escaped. `Hello, World \x26 O\x27Reilly\x21`, `greeting=H%69\x26addressee=(World)`, }, }, { `<button onclick='alert("{{.}}")'>`, []string{ `\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`, `a[href =~ \x22\/\/example.com\x22]#foo`, `Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`, ` dir=\x22ltr\x22`, `c \x26\x26 alert(\x22Hello, World!\x22);`, // Escape sequence not over-escaped. `Hello, World \x26 O\x27Reilly\x21`, `greeting=H%69\x26addressee=(World)`, }, }, { `<a href="?q={{.}}">`, []string{ `%3cb%3e%20%22foo%25%22%20O%27Reilly%20%26bar%3b`, `a%5bhref%20%3d~%20%22%2f%2fexample.com%22%5d%23foo`, `Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21`, `%20dir%3d%22ltr%22`, `c%20%26%26%20alert%28%22Hello%2c%20World%21%22%29%3b`, `Hello%2c%20World%20%26%20O%27Reilly%5cx21`, // Quotes and parens are escaped but %69 is not over-escaped. HTML escaping is done. `greeting=H%69&addressee=%28World%29`, }, }, { `<style>body { background: url('?img={{.}}') }</style>`, []string{ `%3cb%3e%20%22foo%25%22%20O%27Reilly%20%26bar%3b`, `a%5bhref%20%3d~%20%22%2f%2fexample.com%22%5d%23foo`, `Hello%2c%20%3cb%3eWorld%3c%2fb%3e%20%26amp%3btc%21`, `%20dir%3d%22ltr%22`, `c%20%26%26%20alert%28%22Hello%2c%20World%21%22%29%3b`, `Hello%2c%20World%20%26%20O%27Reilly%5cx21`, // Quotes and parens are escaped but %69 is not over-escaped. HTML escaping is not done. `greeting=H%69&addressee=%28World%29`, }, }, } for _, test := range tests { text := fmt.Sprintf(`{{define "t"}}%s{{end}}`, test.input) tmpl, err := new(Set).Parse(text) if err != nil { t.Fatalf("failed parsing %q: %s", text, err) } tmpl.Escape() pre := strings.Index(test.input, "{{.}}") post := len(test.input) - (pre + 5) var b bytes.Buffer for i, x := range data { b.Reset() if err := tmpl.Execute(&b, "t", x); err != nil { t.Errorf("%q with %v: %s", test.input, x, err) continue } if want, got := test.want[i], b.String()[pre:b.Len()-post]; want != got { t.Errorf("%q with %v:\nwant\n\t%q,\ngot\n\t%q\n", test.input, x, want, got) continue } } } }