Skip to content

Commit 3a2fee0

Browse files
Samuel Tanbradfitz
Samuel Tan
authored andcommitted
html/template: allow safe usage of predefined escapers in pipelines
Allow the predefined escapers "html", "urlquery", and "js" to be used in pipelines when they have no potential to affect the correctness or safety of the escaped pipeline output. Specifically: - "urlquery" may be used if it is the last command in the pipeline. - "html" may be used if it is the last command in the pipeline, and the pipeline does not occur in an unquoted HTML attribute value context. - "js" may be used in any pipeline, since it does not affect the merging of contextual escapers. This change will loosens the restrictions on predefined escapers introduced in golang.org/cl/37880, which will hopefully ease the upgrade path for existing template users. This change brings back the escaper-merging logic, and associated unit tests, that were removed in golang.org/cl/37880. However, a few notable changes have been made: - "_html_template_nospaceescaper" is no longer considered equivalent to "html", since the former escapes spaces, while the latter does not (see #19345). This change should not silently break any templates, since pipelines where this substituion will happen will already trigger an explicit error. - An "_eval_args_" internal directive has been added to handle pipelines containing a single explicit call to a predefined escaper, e.g. {{html .X}} (see #19353). Also, the HTMLEscape function called by the predefined text/template "html" function now escapes the NULL character as well. This effectively makes it as secure as the internal html/template HTML escapers (see #19345). While this change is backward-incompatible, it will only affect illegitimate uses of this escaper, since the NULL character is always illegal in valid HTML. Fixes #19952 Change-Id: I9b5570a80a3ea284b53901e6a1f842fc59b33d3a Reviewed-on: https://go-review.googlesource.com/40936 Reviewed-by: Russ Cox <[email protected]> Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 1acff5f commit 3a2fee0

File tree

6 files changed

+240
-41
lines changed

6 files changed

+240
-41
lines changed

src/html/template/doc.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@ functions to each simple action pipeline, so given the excerpt
6565
At parse time each {{.}} is overwritten to add escaping functions as necessary.
6666
In this case it becomes
6767
68-
<a href="/search?q={{. | urlquery}}">{{. | html}}</a>
68+
<a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a>
6969
70+
where urlescaper, attrescaper, and htmlescaper are aliases for internal escaping
71+
functions.
7072
7173
Errors
7274

src/html/template/error.go

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -186,23 +186,30 @@ const (
186186

187187
// ErrPredefinedEscaper: "predefined escaper ... disallowed in template"
188188
// Example:
189-
// <a href="{{.X | urlquery}}">
189+
// <div class={{. | html}}>Hello<div>
190190
// Discussion:
191191
// Package html/template already contextually escapes all pipelines to
192192
// produce HTML output safe against code injection. Manually escaping
193-
// pipeline output using the predefined escapers "html", "urlquery", or "js"
194-
// is unnecessary, and might affect the correctness or safety of the escaped
195-
// pipeline output. In the above example, "urlquery" should simply be
196-
// removed from the pipeline so that escaping is performed solely by the
197-
// contextual autoescaper.
198-
// If the predefined escaper occurs in the middle of a pipeline where
199-
// subsequent commands expect escaped input, e.g.
193+
// pipeline output using the predefined escapers "html" or "urlquery" is
194+
// unnecessary, and may affect the correctness or safety of the escaped
195+
// pipeline output in Go 1.8 and earlier.
196+
//
197+
// In most cases, such as the given example, this error can be resolved by
198+
// simply removing the predefined escaper from the pipeline and letting the
199+
// contextual autoescaper handle the escaping of the pipeline. In other
200+
// instances, where the predefined escaper occurs in the middle of a
201+
// pipeline where subsequent commands expect escaped input, e.g.
200202
// {{.X | html | makeALink}}
201203
// where makeALink does
202-
// return "<a href='+input+'>link</a>"
204+
// return `<a href="`+input+`">link</a>`
203205
// consider refactoring the surrounding template to make use of the
204206
// contextual autoescaper, i.e.
205-
// <a href='{{.X}}'>link</a>
207+
// <a href="{{.X}}">link</a>
208+
//
209+
// To ease migration to Go 1.9 and beyond, "html" and "urlquery" will
210+
// continue to be allowed as the last command in a pipeline. However, if the
211+
// pipeline occurs in an unquoted attribute value context, "html" is
212+
// disallowed. Avoid using "html" and "urlquery" entirely in new templates.
206213
ErrPredefinedEscaper
207214
)
208215

src/html/template/escape.go

Lines changed: 103 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,21 @@ func escapeTemplate(tmpl *Template, node parse.Node, name string) error {
4444
return nil
4545
}
4646

47+
// evalArgs formats the list of arguments into a string. It is equivalent to
48+
// fmt.Sprint(args...), except that it deferences all pointers.
49+
func evalArgs(args ...interface{}) string {
50+
// Optimization for simple common case of a single string argument.
51+
if len(args) == 1 {
52+
if s, ok := args[0].(string); ok {
53+
return s
54+
}
55+
}
56+
for i, arg := range args {
57+
args[i] = indirectToStringerOrError(arg)
58+
}
59+
return fmt.Sprint(args...)
60+
}
61+
4762
// funcMap maps command names to functions that render their inputs safe.
4863
var funcMap = template.FuncMap{
4964
"_html_template_attrescaper": attrEscaper,
@@ -60,13 +75,7 @@ var funcMap = template.FuncMap{
6075
"_html_template_urlescaper": urlEscaper,
6176
"_html_template_urlfilter": urlFilter,
6277
"_html_template_urlnormalizer": urlNormalizer,
63-
}
64-
65-
// predefinedEscapers contains template predefined escapers.
66-
var predefinedEscapers = map[string]bool{
67-
"html": true,
68-
"urlquery": true,
69-
"js": true,
78+
"_eval_args_": evalArgs,
7079
}
7180

7281
// escaper collects type inferences about templates and changes needed to make
@@ -150,18 +159,21 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
150159
// A local variable assignment, not an interpolation.
151160
return c
152161
}
153-
// Disallow the use of predefined escapers in pipelines.
154-
for _, idNode := range n.Pipe.Cmds {
162+
c = nudge(c)
163+
// Check for disallowed use of predefined escapers in the pipeline.
164+
for pos, idNode := range n.Pipe.Cmds {
155165
for _, ident := range allIdents(idNode.Args[0]) {
156166
if _, ok := predefinedEscapers[ident]; ok {
157-
return context{
158-
state: stateError,
159-
err: errorf(ErrPredefinedEscaper, n, n.Line, "predefined escaper %q disallowed in template", ident),
167+
if pos < len(n.Pipe.Cmds)-1 ||
168+
c.state == stateAttr && c.delim == delimSpaceOrTagEnd && ident == "html" {
169+
return context{
170+
state: stateError,
171+
err: errorf(ErrPredefinedEscaper, n, n.Line, "predefined escaper %q disallowed in template", ident),
172+
}
160173
}
161174
}
162175
}
163176
}
164-
c = nudge(c)
165177
s := make([]string, 0, 3)
166178
switch c.state {
167179
case stateError:
@@ -227,21 +239,97 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
227239
}
228240

229241
// ensurePipelineContains ensures that the pipeline ends with the commands with
230-
// the identifiers in s in order.
242+
// the identifiers in s in order. If the pipeline ends with a predefined escaper
243+
// (i.e. "html" or "urlquery"), merge it with the identifiers in s.
231244
func ensurePipelineContains(p *parse.PipeNode, s []string) {
232245
if len(s) == 0 {
233246
// Do not rewrite pipeline if we have no escapers to insert.
234247
return
235248
}
249+
// Precondition: p.Cmds contains at most one predefined escaper and the
250+
// escaper will be present at p.Cmds[len(p.Cmds)-1]. This precondition is
251+
// always true because of the checks in escapeAction.
252+
pipelineLen := len(p.Cmds)
253+
if pipelineLen > 0 {
254+
lastCmd := p.Cmds[pipelineLen-1]
255+
if idNode, ok := lastCmd.Args[0].(*parse.IdentifierNode); ok {
256+
if esc := idNode.Ident; predefinedEscapers[esc] {
257+
// Pipeline ends with a predefined escaper.
258+
if len(p.Cmds) == 1 && len(lastCmd.Args) > 1 {
259+
// Special case: pipeline is of the form {{ esc arg1 arg2 ... argN }},
260+
// where esc is the predefined escaper, and arg1...argN are its arguments.
261+
// Convert this into the equivalent form
262+
// {{ _eval_args_ arg1 arg2 ... argN | esc }}, so that esc can be easily
263+
// merged with the escapers in s.
264+
lastCmd.Args[0] = parse.NewIdentifier("_eval_args_").SetTree(nil).SetPos(lastCmd.Args[0].Position())
265+
p.Cmds = appendCmd(p.Cmds, newIdentCmd(esc, p.Position()))
266+
pipelineLen++
267+
}
268+
// If any of the commands in s that we are about to insert is equivalent
269+
// to the predefined escaper, use the predefined escaper instead.
270+
dup := false
271+
for i, escaper := range s {
272+
if escFnsEq(esc, escaper) {
273+
s[i] = idNode.Ident
274+
dup = true
275+
}
276+
}
277+
if dup {
278+
// The predefined escaper will already be inserted along with the
279+
// escapers in s, so do not copy it to the rewritten pipeline.
280+
pipelineLen--
281+
}
282+
}
283+
}
284+
}
236285
// Rewrite the pipeline, creating the escapers in s at the end of the pipeline.
237-
newCmds := make([]*parse.CommandNode, len(p.Cmds), len(p.Cmds)+len(s))
286+
newCmds := make([]*parse.CommandNode, pipelineLen, pipelineLen+len(s))
238287
copy(newCmds, p.Cmds)
239288
for _, name := range s {
240289
newCmds = appendCmd(newCmds, newIdentCmd(name, p.Position()))
241290
}
242291
p.Cmds = newCmds
243292
}
244293

294+
// predefinedEscapers contains template predefined escapers that are equivalent
295+
// to some contextual escapers. Keep in sync with equivEscapers.
296+
var predefinedEscapers = map[string]bool{
297+
"html": true,
298+
"urlquery": true,
299+
}
300+
301+
// equivEscapers matches contextual escapers to equivalent predefined
302+
// template escapers.
303+
var equivEscapers = map[string]string{
304+
// The following pairs of HTML escapers provide equivalent security
305+
// guarantees, since they all escape '\000', '\'', '"', '&', '<', and '>'.
306+
"_html_template_attrescaper": "html",
307+
"_html_template_htmlescaper": "html",
308+
"_html_template_rcdataescaper": "html",
309+
// These two URL escapers produce URLs safe for embedding in a URL query by
310+
// percent-encoding all the reserved characters specified in RFC 3986 Section
311+
// 2.2
312+
"_html_template_urlescaper": "urlquery",
313+
// These two functions are not actually equivalent; urlquery is stricter as it
314+
// escapes reserved characters (e.g. '#'), while _html_template_urlnormalizer
315+
// does not. It is therefore only safe to replace _html_template_urlnormalizer
316+
// with urlquery (this happens in ensurePipelineContains), but not the otherI've
317+
// way around. We keep this entry around to preserve the behavior of templates
318+
// written before Go 1.9, which might depend on this substitution taking place.
319+
"_html_template_urlnormalizer": "urlquery",
320+
}
321+
322+
// escFnsEq reports whether the two escaping functions are equivalent.
323+
func escFnsEq(a, b string) bool {
324+
if e := equivEscapers[a]; e != "" {
325+
a = e
326+
}
327+
if e := equivEscapers[b]; e != "" {
328+
b = e
329+
}
330+
return a == b
331+
}
332+
245333
// redundantFuncs[a][b] implies that funcMap[b](funcMap[a](x)) == funcMap[a](x)
246334
// for all x.
247335
var redundantFuncs = map[string]map[string]bool{

0 commit comments

Comments
 (0)