At parse time each {{.}} is overwritten to add escaping functions as necessary.
In this case it becomes
- <a href="/search?q={{. | urlquery}}">{{. | html}}</a>
+ <a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a>
+where urlescaper, attrescaper, and htmlescaper are aliases for internal escaping
+functions.
Errors
// ErrPredefinedEscaper: "predefined escaper ... disallowed in template"
// Example:
- // <a href="{{.X | urlquery}}">
+ // <div class={{. | html}}>Hello<div>
// Discussion:
// Package html/template already contextually escapes all pipelines to
// produce HTML output safe against code injection. Manually escaping
- // pipeline output using the predefined escapers "html", "urlquery", or "js"
- // is unnecessary, and might affect the correctness or safety of the escaped
- // pipeline output. In the above example, "urlquery" should simply be
- // removed from the pipeline so that escaping is performed solely by the
- // contextual autoescaper.
- // If the predefined escaper occurs in the middle of a pipeline where
- // subsequent commands expect escaped input, e.g.
+ // pipeline output using the predefined escapers "html" or "urlquery" is
+ // unnecessary, and may affect the correctness or safety of the escaped
+ // pipeline output in Go 1.8 and earlier.
+ //
+ // In most cases, such as the given example, this error can be resolved by
+ // simply removing the predefined escaper from the pipeline and letting the
+ // contextual autoescaper handle the escaping of the pipeline. In other
+ // instances, where the predefined escaper occurs in the middle of a
+ // pipeline where subsequent commands expect escaped input, e.g.
// {{.X | html | makeALink}}
// where makeALink does
- // return "<a href='+input+'>link</a>"
+ // return `<a href="`+input+`">link</a>`
// consider refactoring the surrounding template to make use of the
// contextual autoescaper, i.e.
- // <a href='{{.X}}'>link</a>
+ // <a href="{{.X}}">link</a>
+ //
+ // To ease migration to Go 1.9 and beyond, "html" and "urlquery" will
+ // continue to be allowed as the last command in a pipeline. However, if the
+ // pipeline occurs in an unquoted attribute value context, "html" is
+ // disallowed. Avoid using "html" and "urlquery" entirely in new templates.
ErrPredefinedEscaper
)
return nil
}
+// evalArgs formats the list of arguments into a string. It is equivalent to
+// fmt.Sprint(args...), except that it deferences all pointers.
+func evalArgs(args ...interface{}) string {
+ // Optimization for simple common case of a single string argument.
+ if len(args) == 1 {
+ if s, ok := args[0].(string); ok {
+ return s
+ }
+ }
+ for i, arg := range args {
+ args[i] = indirectToStringerOrError(arg)
+ }
+ return fmt.Sprint(args...)
+}
+
// funcMap maps command names to functions that render their inputs safe.
var funcMap = template.FuncMap{
"_html_template_attrescaper": attrEscaper,
"_html_template_urlescaper": urlEscaper,
"_html_template_urlfilter": urlFilter,
"_html_template_urlnormalizer": urlNormalizer,
-}
-
-// predefinedEscapers contains template predefined escapers.
-var predefinedEscapers = map[string]bool{
- "html": true,
- "urlquery": true,
- "js": true,
+ "_eval_args_": evalArgs,
}
// escaper collects type inferences about templates and changes needed to make
// A local variable assignment, not an interpolation.
return c
}
- // Disallow the use of predefined escapers in pipelines.
- for _, idNode := range n.Pipe.Cmds {
+ c = nudge(c)
+ // Check for disallowed use of predefined escapers in the pipeline.
+ for pos, idNode := range n.Pipe.Cmds {
for _, ident := range allIdents(idNode.Args[0]) {
if _, ok := predefinedEscapers[ident]; ok {
- return context{
- state: stateError,
- err: errorf(ErrPredefinedEscaper, n, n.Line, "predefined escaper %q disallowed in template", ident),
+ if pos < len(n.Pipe.Cmds)-1 ||
+ c.state == stateAttr && c.delim == delimSpaceOrTagEnd && ident == "html" {
+ return context{
+ state: stateError,
+ err: errorf(ErrPredefinedEscaper, n, n.Line, "predefined escaper %q disallowed in template", ident),
+ }
}
}
}
}
- c = nudge(c)
s := make([]string, 0, 3)
switch c.state {
case stateError:
}
// ensurePipelineContains ensures that the pipeline ends with the commands with
-// the identifiers in s in order.
+// the identifiers in s in order. If the pipeline ends with a predefined escaper
+// (i.e. "html" or "urlquery"), merge it with the identifiers in s.
func ensurePipelineContains(p *parse.PipeNode, s []string) {
if len(s) == 0 {
// Do not rewrite pipeline if we have no escapers to insert.
return
}
+ // Precondition: p.Cmds contains at most one predefined escaper and the
+ // escaper will be present at p.Cmds[len(p.Cmds)-1]. This precondition is
+ // always true because of the checks in escapeAction.
+ pipelineLen := len(p.Cmds)
+ if pipelineLen > 0 {
+ lastCmd := p.Cmds[pipelineLen-1]
+ if idNode, ok := lastCmd.Args[0].(*parse.IdentifierNode); ok {
+ if esc := idNode.Ident; predefinedEscapers[esc] {
+ // Pipeline ends with a predefined escaper.
+ if len(p.Cmds) == 1 && len(lastCmd.Args) > 1 {
+ // Special case: pipeline is of the form {{ esc arg1 arg2 ... argN }},
+ // where esc is the predefined escaper, and arg1...argN are its arguments.
+ // Convert this into the equivalent form
+ // {{ _eval_args_ arg1 arg2 ... argN | esc }}, so that esc can be easily
+ // merged with the escapers in s.
+ lastCmd.Args[0] = parse.NewIdentifier("_eval_args_").SetTree(nil).SetPos(lastCmd.Args[0].Position())
+ p.Cmds = appendCmd(p.Cmds, newIdentCmd(esc, p.Position()))
+ pipelineLen++
+ }
+ // If any of the commands in s that we are about to insert is equivalent
+ // to the predefined escaper, use the predefined escaper instead.
+ dup := false
+ for i, escaper := range s {
+ if escFnsEq(esc, escaper) {
+ s[i] = idNode.Ident
+ dup = true
+ }
+ }
+ if dup {
+ // The predefined escaper will already be inserted along with the
+ // escapers in s, so do not copy it to the rewritten pipeline.
+ pipelineLen--
+ }
+ }
+ }
+ }
// Rewrite the pipeline, creating the escapers in s at the end of the pipeline.
- newCmds := make([]*parse.CommandNode, len(p.Cmds), len(p.Cmds)+len(s))
+ newCmds := make([]*parse.CommandNode, pipelineLen, pipelineLen+len(s))
copy(newCmds, p.Cmds)
for _, name := range s {
newCmds = appendCmd(newCmds, newIdentCmd(name, p.Position()))
p.Cmds = newCmds
}
+// predefinedEscapers contains template predefined escapers that are equivalent
+// to some contextual escapers. Keep in sync with equivEscapers.
+var predefinedEscapers = map[string]bool{
+ "html": true,
+ "urlquery": true,
+}
+
+// equivEscapers matches contextual escapers to equivalent predefined
+// template escapers.
+var equivEscapers = map[string]string{
+ // The following pairs of HTML escapers provide equivalent security
+ // guarantees, since they all escape '\000', '\'', '"', '&', '<', and '>'.
+ "_html_template_attrescaper": "html",
+ "_html_template_htmlescaper": "html",
+ "_html_template_rcdataescaper": "html",
+ // These two URL escapers produce URLs safe for embedding in a URL query by
+ // percent-encoding all the reserved characters specified in RFC 3986 Section
+ // 2.2
+ "_html_template_urlescaper": "urlquery",
+ // These two functions are not actually equivalent; urlquery is stricter as it
+ // escapes reserved characters (e.g. '#'), while _html_template_urlnormalizer
+ // does not. It is therefore only safe to replace _html_template_urlnormalizer
+ // with urlquery (this happens in ensurePipelineContains), but not the otherI've
+ // way around. We keep this entry around to preserve the behavior of templates
+ // written before Go 1.9, which might depend on this substitution taking place.
+ "_html_template_urlnormalizer": "urlquery",
+}
+
+// escFnsEq reports whether the two escaping functions are equivalent.
+func escFnsEq(a, b string) bool {
+ if e := equivEscapers[a]; e != "" {
+ a = e
+ }
+ if e := equivEscapers[b]; e != "" {
+ b = e
+ }
+ return a == b
+}
+
// redundantFuncs[a][b] implies that funcMap[b](funcMap[a](x)) == funcMap[a](x)
// for all x.
var redundantFuncs = map[string]map[string]bool{
"<Goodbye>!",
},
{
- "overescaping",
+ "overescaping1",
+ "Hello, {{.C | html}}!",
+ "Hello, <Cincinatti>!",
+ },
+ {
+ "overescaping2",
+ "Hello, {{html .C}}!",
+ "Hello, <Cincinatti>!",
+ },
+ {
+ "overescaping3",
"{{with .C}}{{$msg := .}}Hello, {{$msg}}!{{end}}",
"Hello, <Cincinatti>!",
},
"<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({{.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(""))'>`,
`: expected space, attr name, or end of tag, but got "=foo>"`,
},
{
- `Hello, {{. | html}}!`,
- // Piping to html is disallowed.
- `predefined escaper "html" disallowed in template`,
+ `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, even if it is not the last command in the pipeline.
+ // html is disallowed if it is not the last command in the pipeline.
`predefined escaper "html" disallowed in template`,
},
{
- `Hello, {{html .}}!`,
- // Calling html is disallowed.
+ `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`,
},
{
- `Hello, {{. | urlquery | html}}!`,
- // urlquery is disallowed; first disallowed escaper in the pipeline is reported in error.
- `predefined escaper "urlquery" 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`,
},
{
- `<script>function do{{. | js}}() { return 1 }</script>`,
- // js is disallowed.
- `predefined escaper "js" 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`,
},
}
for _, test := range tests {
".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",
".X | (print 12 | js).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"},
+ },
+ {
+ "{{.X | html}}",
+ ".X | html | html",
+ []string{"_html_template_htmlescaper", "_html_template_attrescaper"},
+ },
+ {
+ "{{.X | html}}",
+ ".X | html | html",
+ []string{"_html_template_rcdataescaper", "_html_template_attrescaper"},
+ },
}
for i, test := range tests {
tmpl := template.Must(template.New("test").Parse(test.input))
func TestEscapeMalformedPipelines(t *testing.T) {
tests := []string{
"{{ 0 | $ }}",
+ "{{ 0 | $ | urlquery }}",
"{{ 0 | (nil) }}",
+ "{{ 0 | (nil) | html }}",
}
for _, test := range tests {
var b bytes.Buffer
or the returned error value is non-nil, execution stops.
html
Returns the escaped HTML equivalent of the textual
- representation of its arguments.
+ representation of its arguments. This function is unavailable
+ in html/template, with a few exceptions.
index
Returns the result of indexing its first argument by the
following arguments. Thus "index x 1 2 3" is, in Go syntax,
urlquery
Returns the escaped value of the textual representation of
its arguments in a form suitable for embedding in a URL query.
+ This function is unavailable in html/template, with a few
+ exceptions.
The boolean functions take any zero value to be false and a non-zero
value to be true.
htmlAmp = []byte("&")
htmlLt = []byte("<")
htmlGt = []byte(">")
+ htmlNull = []byte("\uFFFD")
)
// HTMLEscape writes to w the escaped HTML equivalent of the plain text data b.
for i, c := range b {
var html []byte
switch c {
+ case '\000':
+ html = htmlNull
case '"':
html = htmlQuot
case '\'':
// HTMLEscapeString returns the escaped HTML equivalent of the plain text data s.
func HTMLEscapeString(s string) string {
// Avoid allocation if we can.
- if !strings.ContainsAny(s, `'"&<>`) {
+ if !strings.ContainsAny(s, "'\"&<>\000") {
return s
}
var b bytes.Buffer