2019-12-10 02:02:15 -05:00
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
2022-03-16 03:48:16 -04:00
//go:build go1.13 && !windows
2019-12-10 02:02:15 -05:00
// +build go1.13,!windows
package template
import (
"bytes"
"encoding/json"
"fmt"
htmltemplate "html/template"
"os"
"strings"
"testing"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
)
type badMarshaler struct { }
func ( x * badMarshaler ) MarshalJSON ( ) ( [ ] byte , error ) {
// Keys in valid JSON must be double quoted as must all strings.
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 string
A , E [ ] string
B , M json . Marshaler
N int
2022-03-16 03:48:16 -04:00
U any // untyped nil
Z * int // typed nil
2019-12-10 02:02:15 -05:00
W htmltemplate . 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 : htmltemplate . 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, <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}}")'>" ,
2020-05-23 09:32:27 -04:00
` <button onclick='alert("\u003cHello\u003e")'> ` ,
2019-12-10 02:02:15 -05:00
} ,
{
"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("%3CCincinnati%3E")'> ` ,
} ,
{
"jsRe" ,
` <button onclick='alert(/ {{ "foo+bar" }} /.test(""))'> ` ,
2020-05-23 09:32:27 -04:00
` <button onclick='alert(/foo\u002bbar/.test(""))'> ` ,
2019-12-10 02:02:15 -05:00
} ,
{
"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 contain 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, <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>" ,
// 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("\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 }} ` +
// 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> ` ,
} ,
{
"srcset bad URL in second position" ,
` <img srcset=" {{ "/not-an-image#,javascript:alert(1)" }} "> ` ,
// The second URL is also filtered.
` <img srcset="/not-an-image#,#ZgotmplZ"> ` ,
} ,
{
"srcset buffer growth" ,
` <img srcset= {{ ",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,," }} > ` ,
` <img srcset=,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,> ` ,
} ,
2023-06-15 10:34:16 -04:00
{
"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=\"\">" ,
} ,
2019-12-10 02:02:15 -05:00
}
for _ , test := range tests {
2023-06-15 10:34:16 -04:00
t . Run ( test . name , func ( t * testing . T ) {
tmpl := New ( test . name )
tmpl = Must ( tmpl . Parse ( test . input ) )
// Check for bug 6459: Tree field was not set in Parse.
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 )
}
} )
2019-12-10 02:02:15 -05:00
}
}
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
} {
// covering issue 20323
{
"field with predefined escaper name 1" ,
` {{ .html | print }} ` ,
` <h1>Hi!</h1> ` ,
} ,
// covering issue 20323
{
"field with predefined escaper name 2" ,
` {{ .urlquery | print }} ` ,
` http://www.foo.com/index.html?title=main ` ,
} ,
} {
tmpl := Must ( New ( "" ) . Parse ( test . input ) )
2022-11-14 13:13:09 -05:00
b := new ( strings . Builder )
2019-12-10 02:02:15 -05:00
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
} {
// The trivial set.
{
map [ string ] string {
"main" : ` ` ,
} ,
` ` ,
} ,
// A template called in the start context.
{
map [ string ] string {
"main" : ` Hello, {{ template "helper" }} ! ` ,
// Not a valid top level HTML template.
// "<b" is not a full tag.
"helper" : ` {{ "<World>" }} ` ,
} ,
` Hello, <World>! ` ,
} ,
// A template called in a context other than the start.
{
map [ string ] string {
"main" : ` <a onclick='a = {{ template "helper" }} ;'> ` ,
// Not a valid top level HTML template.
// "<b" is not a full tag.
"helper" : ` {{ "<a>" }} <b ` ,
} ,
` <a onclick='a = "\u003ca\u003e"<b;'> ` ,
} ,
// A recursive template that ends in its start context.
{
map [ string ] string {
"main" : ` {{ range .Children }} {{ template "main" . }} {{ else }} {{ .X }} {{ end }} ` ,
} ,
` foo <bar> baz ` ,
} ,
// A recursive helper template that ends in its start context.
{
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> ` ,
} ,
// Co-recursive templates that end in its start context.
{
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> ` ,
} ,
// A template that is called in two different contexts.
{
map [ string ] string {
"main" : ` <button onclick="title=' {{ template "helper" }} '; ..."> {{ template "helper" }} </button> ` ,
"helper" : ` {{ 11 }} of {{ "<100>" }} ` ,
} ,
2020-05-23 09:32:27 -04:00
` <button onclick="title='11 of \u003c100\u003e'; ...">11 of <100></button> ` ,
2019-12-10 02:02:15 -05:00
} ,
// A non-recursive template that ends in a different context.
// helper starts in jsCtxRegexp and ends in jsCtxDivOp.
{
map [ string ] string {
"main" : ` <script>var x= {{ template "helper" }} / {{ "42" }} ;</script> ` ,
"helper" : "{{126}}" ,
} ,
` <script>var x= 126 /"42";</script> ` ,
} ,
// A recursive template that ends in a similar context.
{
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> ` ,
} ,
// A recursive template that ends in a different context.
/ *
{
map [ string ] string {
"main" : ` <a href="/foo {{ template "helper" . }} "> ` ,
"helper" : ` {{ if .Children }} {{ range .Children }} {{ template "helper" . }} {{ end }} {{ else }} ?x= {{ .X }} {{ end }} ` ,
} ,
` <a href="/foo?x=foo?x=%3cbar%3e?x=baz"> ` ,
} ,
* /
}
// pred is a template function that returns the predecessor of a
// natural number for testing recursive templates.
2022-03-16 03:48:16 -04:00
fns := FuncMap { "pred" : func ( a ... any ) ( any , error ) {
2019-12-10 02:02:15 -05:00
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
}
2022-11-14 13:13:09 -05:00
var b strings . Builder
2019-12-10 02:02:15 -05:00
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 )
}
}
2020-12-03 07:47:43 -05:00
2019-12-10 02:02:15 -05:00
}
func TestErrors ( t * testing . T ) {
tests := [ ] struct {
input string
err string
} {
// Non-error cases.
{
"{{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}}'>" ,
"" ,
} ,
2022-03-16 03:48:16 -04:00
{
"{{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}}" ,
"" ,
} ,
2023-06-15 10:34:16 -04:00
{
"<script>var a = `${a+b}`</script>`" ,
"" ,
} ,
2019-12-10 02:02:15 -05:00
// Error cases.
{
"{{if .Cond}}<a{{end}}" ,
"z:1:5: {{if}} branches" ,
} ,
{
"{{if .Cond}}\n{{else}}\n<a{{end}}" ,
"z:1:5: {{if}} branches" ,
} ,
{
// Missing quote in the else branch.
` {{ if .Cond }} <a href="foo"> {{ else }} <a href="bar> {{ end }} ` ,
"z:1:5: {{if}} branches" ,
} ,
{
// Different kind of attribute: href implies a URL.
"<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" ,
} ,
2022-03-16 03:48:16 -04:00
{
"{{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" ,
} ,
2019-12-10 02:02:15 -05:00
{
"<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[\\]/" ` ,
} ,
{
// It is ambiguous whether 1.5 should be 1\.5 or 1.5.
// Either `var x = 1/- 1.5 /i.test(x)`
// where `i.test(x)` is a method call of reference i,
// or `/-1\.5/i.test(x)` which is a method call on a
// case insensitive regular expression.
` <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" }} > ` +
// Illegal starting in stateTag but not in stateText.
` {{ define "y" }} foo<b {{ end }} ` ,
` "<" in attribute name: " foo<b" ` ,
} ,
{
` <script>reverseList = [ {{ template "t" }} ]</script> ` +
// Missing " after recursive call.
` {{ 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 }} ! ` ,
// urlquery is disallowed if it is not the last command in the pipeline.
` predefined escaper "urlquery" disallowed in template ` ,
} ,
{
` Hello, {{ . | html | print }} ! ` ,
// html is disallowed if it is not the last command in the pipeline.
` predefined escaper "html" disallowed in template ` ,
} ,
{
` Hello, {{ html . | print }} ! ` ,
// A direct call to html is disallowed if it is not the last command in the pipeline.
` predefined escaper "html" disallowed in template ` ,
} ,
{
` <div class= {{ . | html }} >Hello<div> ` ,
// html is disallowed in a pipeline that is in an unquoted attribute context,
// even if it is the last command in the pipeline.
` predefined escaper "html" disallowed in template ` ,
} ,
{
` Hello, {{ . | urlquery | html }} ! ` ,
// html is allowed since it is the last command in the pipeline, but urlquery is not.
` predefined escaper "urlquery" disallowed in template ` ,
} ,
2023-06-15 10:34:16 -04:00
{
"<script>var tmpl = `asd {{.}}`;</script>" ,
` {{ . }} appears in a JS template literal ` ,
} ,
2019-12-10 02:02:15 -05:00
}
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
}
// Check that we get the same error if we call Execute again.
if err := tmpl . Execute ( buf , nil ) ; err == nil || err . Error ( ) != got {
t . Errorf ( "input=%q: unexpected error on second call %q" , test . input , err )
2020-12-03 07:47:43 -05:00
2019-12-10 02:02:15 -05:00
}
}
}
func TestEscapeText ( t * testing . T ) {
tests := [ ] struct {
input string
output context
} {
{
` ` ,
context { } ,
} ,
{
` Hello, World! ` ,
context { } ,
} ,
{
// An orphaned "<" is OK.
` 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 } ,
} ,
2023-06-15 10:34:16 -04:00
{
"<a onclick=\"`foo" ,
context { state : stateJSBqStr , delim : delimDoubleQuote , attr : attrScript } ,
} ,
2019-12-10 02:02:15 -05:00
{
` <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 : stateText } ,
} ,
{
` <script type="text/template"> ` ,
context { state : stateText } ,
} ,
// covering issue 19968
{
` <script type="TEXT/JAVASCRIPT"> ` ,
context { state : stateJS , element : elementScript } ,
} ,
// covering issue 19965
{
` <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 { } ,
} ,
}
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" } ,
} ,
{
// covering issue 10801
"{{.X | println.x }}" ,
".X | println.x | urlquery | html" ,
[ ] string { "urlquery" , "html" } ,
} ,
{
// covering issue 10801
"{{.X | (print 12 | println).x }}" ,
".X | (print 12 | println).x | urlquery | html" ,
[ ] string { "urlquery" , "html" } ,
} ,
// The following test cases ensure that the merging of internal escapers
// with the predefined "html" and "urlquery" escapers is correct.
{
"{{.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 ) {
2022-03-16 03:48:16 -04:00
inputs := [ ] any {
2019-12-10 02:02:15 -05:00
"\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\\" ,
htmltemplate . CSS ( ` a[href =~ "//example.com"]#foo ` ) ,
htmltemplate . HTML ( ` Hello, <b>World</b> &tc! ` ) ,
htmltemplate . HTMLAttr ( ` dir="ltr" ` ) ,
htmltemplate . JS ( ` c && alert("Hello, World!"); ` ) ,
htmltemplate . JSStr ( ` Hello, World & O'Reilly\x21 ` ) ,
htmltemplate . URL ( ` greeting=H%69&addressee=(World) ` ) ,
}
for n0 , m := range redundantFuncs {
2022-03-16 03:48:16 -04:00
f0 := funcMap [ n0 ] . ( func ( ... any ) string )
2019-12-10 02:02:15 -05:00
for n1 := range m {
2022-03-16 03:48:16 -04:00
f1 := funcMap [ n1 ] . ( func ( ... any ) string )
2019-12-10 02:02:15 -05:00
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 ( ` {{ . }} ` ) )
2022-11-14 13:13:09 -05:00
var buf strings . Builder
2019-12-10 02:02:15 -05:00
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 ( ) )
}
}
// This is a test for issue 3272.
2020-12-03 07:50:17 -05:00
func TestEmptyTemplateHTML ( t * testing . T ) {
2019-12-10 02:02:15 -05:00
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 )
}
// This is a test for issue 7379: type assertion error caused panic, and then
// the code to handle the panic breaks escaping. It's hard to see the second
// problem once the first is fixed, but its fix is trivial so we let that go. See
// the discussion for issue 7379.
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 )
}
} ( )
2022-11-14 13:13:09 -05:00
var b strings . Builder
2019-12-10 02:02:15 -05:00
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 )
}
}
}
// Unlike text/template, html/template crashed if given an incomplete
// template, that is, a template that had been named but not given any content.
// This is issue #10204.
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 )
}
}
// This covers issue #20842.
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 }} ` ) )
2022-11-14 13:13:09 -05:00
got := new ( strings . Builder )
2019-12-10 02:02:15 -05:00
var err error
// Ensure that "hello" produces the same output when executed twice.
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 ( )
}
// Ensure that the implicit re-execution of "hello" during the execution of
// "main" does not cause the output of "hello" to change.
err = tmpl . ExecuteTemplate ( got , "main" , nil )
if err != nil {
t . Errorf ( "unexpected error: %s" , err )
}
// If the HTML escaper is added again to the action {{"Ladies & Gentlemen!"}},
// we would expected to see the ampersand overescaped to "&amp;".
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 ( )
}
}
// Covers issue 22780.
func TestOrphanedTemplate ( t * testing . T ) {
t1 := Must ( New ( "foo" ) . Parse ( ` <a href=" {{ . }} ">link1</a> ` ) )
t2 := Must ( t1 . New ( "foo" ) . Parse ( ` bar ` ) )
2022-11-14 13:13:09 -05:00
var b strings . Builder
2019-12-10 02:02:15 -05:00
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 )
}
}
// Covers issue 21844.
func TestAliasedParseTreeDoesNotOverescape ( t * testing . T ) {
const (
tmplText = ` {{ . }} `
data = ` <baz> `
want = ` <baz> `
)
// Templates "foo" and "bar" both alias the same underlying parse tree.
tpl := Must ( New ( "foo" ) . Parse ( tmplText ) )
if _ , err := tpl . AddParseTree ( "bar" , tpl . Tree ) ; err != nil {
t . Fatalf ( "AddParseTree error: %v" , err )
}
2022-11-14 13:13:09 -05:00
var b1 , b2 strings . Builder
2019-12-10 02:02:15 -05:00
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 )
}
}