diff options
author | Ian Lance Taylor <ian@gcc.gnu.org> | 2018-01-17 14:20:29 +0000 |
---|---|---|
committer | Ian Lance Taylor <ian@gcc.gnu.org> | 2018-01-17 14:20:29 +0000 |
commit | c6d6367f848cfd8381aba41e035c5e7e873667c5 (patch) | |
tree | a218e98243463fc27f5053b4444e2544c63cd57a /libgo/go/html | |
parent | 9bff0086915f544fa648ea81131f035cb9ce79a4 (diff) | |
download | gcc-c6d6367f848cfd8381aba41e035c5e7e873667c5.zip gcc-c6d6367f848cfd8381aba41e035c5e7e873667c5.tar.gz gcc-c6d6367f848cfd8381aba41e035c5e7e873667c5.tar.bz2 |
libgo: update to Go1.10beta2 release
Reviewed-on: https://go-review.googlesource.com/87897
From-SVN: r256794
Diffstat (limited to 'libgo/go/html')
-rw-r--r-- | libgo/go/html/template/attr.go | 1 | ||||
-rw-r--r-- | libgo/go/html/template/content.go | 11 | ||||
-rw-r--r-- | libgo/go/html/template/content_test.go | 166 | ||||
-rw-r--r-- | libgo/go/html/template/context.go | 6 | ||||
-rw-r--r-- | libgo/go/html/template/escape.go | 35 | ||||
-rw-r--r-- | libgo/go/html/template/escape_test.go | 77 | ||||
-rw-r--r-- | libgo/go/html/template/template.go | 13 | ||||
-rw-r--r-- | libgo/go/html/template/transition.go | 4 | ||||
-rw-r--r-- | libgo/go/html/template/url.go | 108 | ||||
-rw-r--r-- | libgo/go/html/template/url_test.go | 57 |
10 files changed, 427 insertions, 51 deletions
diff --git a/libgo/go/html/template/attr.go b/libgo/go/html/template/attr.go index 7438f51..92d2789 100644 --- a/libgo/go/html/template/attr.go +++ b/libgo/go/html/template/attr.go @@ -120,6 +120,7 @@ var attrTypeMap = map[string]contentType{ "src": contentTypeURL, "srcdoc": contentTypeHTML, "srclang": contentTypePlain, + "srcset": contentTypeSrcset, "start": contentTypePlain, "step": contentTypePlain, "style": contentTypeCSS, diff --git a/libgo/go/html/template/content.go b/libgo/go/html/template/content.go index 2e14bd1..e7cdedc 100644 --- a/libgo/go/html/template/content.go +++ b/libgo/go/html/template/content.go @@ -83,6 +83,14 @@ type ( // the encapsulated content should come from a trusted source, // as it will be included verbatim in the template output. URL string + + // Srcset encapsulates a known safe srcset attribute + // (see http://w3c.github.io/html/semantics-embedded-content.html#element-attrdef-img-srcset). + // + // Use of this type presents a security risk: + // the encapsulated content should come from a trusted source, + // as it will be included verbatim in the template output. + Srcset string ) type contentType uint8 @@ -95,6 +103,7 @@ const ( contentTypeJS contentTypeJSStr contentTypeURL + contentTypeSrcset // contentTypeUnsafe is used in attr.go for values that affect how // embedded content and network messages are formed, vetted, // or interpreted; or which credentials network messages carry. @@ -156,6 +165,8 @@ func stringify(args ...interface{}) (string, contentType) { return string(s), contentTypeJSStr case URL: return string(s), contentTypeURL + case Srcset: + return string(s), contentTypeSrcset } } for i, arg := range args { diff --git a/libgo/go/html/template/content_test.go b/libgo/go/html/template/content_test.go index 0b4365c..cc092f5 100644 --- a/libgo/go/html/template/content_test.go +++ b/libgo/go/html/template/content_test.go @@ -19,7 +19,9 @@ func TestTypedContent(t *testing.T) { HTMLAttr(` dir="ltr"`), JS(`c && alert("Hello, World!");`), JSStr(`Hello, World & O'Reilly\x21`), - URL(`greeting=H%69&addressee=(World)`), + URL(`greeting=H%69,&addressee=(World)`), + Srcset(`greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`), + URL(`,foo/,`), } // For each content sensitive escaper, see how it does on @@ -40,6 +42,8 @@ func TestTypedContent(t *testing.T) { `ZgotmplZ`, `ZgotmplZ`, `ZgotmplZ`, + `ZgotmplZ`, + `ZgotmplZ`, }, }, { @@ -53,6 +57,8 @@ func TestTypedContent(t *testing.T) { `ZgotmplZ`, `ZgotmplZ`, `ZgotmplZ`, + `ZgotmplZ`, + `ZgotmplZ`, }, }, { @@ -65,7 +71,9 @@ func TestTypedContent(t *testing.T) { ` dir="ltr"`, `c && alert("Hello, World!");`, `Hello, World & O'Reilly\x21`, - `greeting=H%69&addressee=(World)`, + `greeting=H%69,&addressee=(World)`, + `greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`, + `,foo/,`, }, }, { @@ -79,6 +87,8 @@ func TestTypedContent(t *testing.T) { `ZgotmplZ`, `ZgotmplZ`, `ZgotmplZ`, + `ZgotmplZ`, + `ZgotmplZ`, }, }, { @@ -91,7 +101,9 @@ func TestTypedContent(t *testing.T) { ` dir="ltr"`, `c && alert("Hello, World!");`, `Hello, World & O'Reilly\x21`, - `greeting=H%69&addressee=(World)`, + `greeting=H%69,&addressee=(World)`, + `greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`, + `,foo/,`, }, }, { @@ -104,7 +116,9 @@ func TestTypedContent(t *testing.T) { ` dir="ltr"`, `c && alert("Hello, World!");`, `Hello, World & O'Reilly\x21`, - `greeting=H%69&addressee=(World)`, + `greeting=H%69,&addressee=(World)`, + `greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`, + `,foo/,`, }, }, { @@ -117,7 +131,9 @@ func TestTypedContent(t *testing.T) { ` dir="ltr"`, `c && alert("Hello, World!");`, `Hello, World & O'Reilly\x21`, - `greeting=H%69&addressee=(World)`, + `greeting=H%69,&addressee=(World)`, + `greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`, + `,foo/,`, }, }, { @@ -131,7 +147,9 @@ func TestTypedContent(t *testing.T) { `c && alert("Hello, World!");`, // Escape sequence not over-escaped. `"Hello, World & O'Reilly\x21"`, - `"greeting=H%69\u0026addressee=(World)"`, + `"greeting=H%69,\u0026addressee=(World)"`, + `"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`, + `",foo/,"`, }, }, { @@ -145,7 +163,9 @@ func TestTypedContent(t *testing.T) { `c && alert("Hello, World!");`, // Escape sequence not over-escaped. `"Hello, World & O'Reilly\x21"`, - `"greeting=H%69\u0026addressee=(World)"`, + `"greeting=H%69,\u0026addressee=(World)"`, + `"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`, + `",foo/,"`, }, }, { @@ -158,7 +178,9 @@ func TestTypedContent(t *testing.T) { `c \x26\x26 alert(\x22Hello, World!\x22);`, // Escape sequence not over-escaped. `Hello, World \x26 O\x27Reilly\x21`, - `greeting=H%69\x26addressee=(World)`, + `greeting=H%69,\x26addressee=(World)`, + `greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`, + `,foo\/,`, }, }, { @@ -171,7 +193,9 @@ func TestTypedContent(t *testing.T) { `c \x26\x26 alert(\x22Hello, World!\x22);`, // Escape sequence not over-escaped. `Hello, World \x26 O\x27Reilly\x21`, - `greeting=H%69\x26addressee=(World)`, + `greeting=H%69,\x26addressee=(World)`, + `greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`, + `,foo\/,`, }, }, { @@ -185,7 +209,9 @@ func TestTypedContent(t *testing.T) { `c && alert("Hello, World!");`, // Escape sequence not over-escaped. `"Hello, World & O'Reilly\x21"`, - `"greeting=H%69\u0026addressee=(World)"`, + `"greeting=H%69,\u0026addressee=(World)"`, + `"greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w"`, + `",foo/,"`, }, }, { @@ -199,7 +225,9 @@ func TestTypedContent(t *testing.T) { ` dir="ltr"`, `c && alert("Hello, World!");`, `Hello, World & O'Reilly\x21`, - `greeting=H%69&addressee=(World)`, + `greeting=H%69,&addressee=(World)`, + `greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`, + `,foo/,`, }, }, { @@ -212,7 +240,9 @@ func TestTypedContent(t *testing.T) { `c \x26\x26 alert(\x22Hello, World!\x22);`, // Escape sequence not over-escaped. `Hello, World \x26 O\x27Reilly\x21`, - `greeting=H%69\x26addressee=(World)`, + `greeting=H%69,\x26addressee=(World)`, + `greeting=H%69,\x26addressee=(World) 2x, https:\/\/golang.org\/favicon.ico 500.5w`, + `,foo\/,`, }, }, { @@ -225,7 +255,9 @@ func TestTypedContent(t *testing.T) { `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`, + `greeting=H%69,&addressee=%28World%29`, + `greeting%3dH%2569%2c%26addressee%3d%28World%29%202x%2c%20https%3a%2f%2fgolang.org%2ffavicon.ico%20500.5w`, + `,foo/,`, }, }, { @@ -238,7 +270,113 @@ func TestTypedContent(t *testing.T) { `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`, + `greeting=H%69,&addressee=%28World%29`, + `greeting%3dH%2569%2c%26addressee%3d%28World%29%202x%2c%20https%3a%2f%2fgolang.org%2ffavicon.ico%20500.5w`, + `,foo/,`, + }, + }, + { + `<img srcset="{{.}}">`, + []string{ + `#ZgotmplZ`, + `#ZgotmplZ`, + // Commas are not esacped + `Hello,#ZgotmplZ`, + // Leading spaces are not percent escapes. + ` dir=%22ltr%22`, + // Spaces after commas are not percent escaped. + `#ZgotmplZ, World!%22%29;`, + `Hello,#ZgotmplZ`, + `greeting=H%69%2c&addressee=%28World%29`, + // Metadata is not escaped. + `greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`, + `%2cfoo/%2c`, + }, + }, + { + `<img srcset={{.}}>`, + []string{ + `#ZgotmplZ`, + `#ZgotmplZ`, + `Hello,#ZgotmplZ`, + // Spaces are HTML escaped not %-escaped + ` dir=%22ltr%22`, + `#ZgotmplZ, World!%22%29;`, + `Hello,#ZgotmplZ`, + `greeting=H%69%2c&addressee=%28World%29`, + `greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`, + // Commas are escaped. + `%2cfoo/%2c`, + }, + }, + { + `<img srcset="{{.}} 2x, https://golang.org/ 500.5w">`, + []string{ + `#ZgotmplZ`, + `#ZgotmplZ`, + `Hello,#ZgotmplZ`, + ` dir=%22ltr%22`, + `#ZgotmplZ, World!%22%29;`, + `Hello,#ZgotmplZ`, + `greeting=H%69%2c&addressee=%28World%29`, + `greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`, + `%2cfoo/%2c`, + }, + }, + { + `<img srcset="http://godoc.org/ {{.}}, https://golang.org/ 500.5w">`, + []string{ + `#ZgotmplZ`, + `#ZgotmplZ`, + `Hello,#ZgotmplZ`, + ` dir=%22ltr%22`, + `#ZgotmplZ, World!%22%29;`, + `Hello,#ZgotmplZ`, + `greeting=H%69%2c&addressee=%28World%29`, + `greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`, + `%2cfoo/%2c`, + }, + }, + { + `<img srcset="http://godoc.org/?q={{.}} 2x, https://golang.org/ 500.5w">`, + []string{ + `#ZgotmplZ`, + `#ZgotmplZ`, + `Hello,#ZgotmplZ`, + ` dir=%22ltr%22`, + `#ZgotmplZ, World!%22%29;`, + `Hello,#ZgotmplZ`, + `greeting=H%69%2c&addressee=%28World%29`, + `greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`, + `%2cfoo/%2c`, + }, + }, + { + `<img srcset="http://godoc.org/ 2x, {{.}} 500.5w">`, + []string{ + `#ZgotmplZ`, + `#ZgotmplZ`, + `Hello,#ZgotmplZ`, + ` dir=%22ltr%22`, + `#ZgotmplZ, World!%22%29;`, + `Hello,#ZgotmplZ`, + `greeting=H%69%2c&addressee=%28World%29`, + `greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`, + `%2cfoo/%2c`, + }, + }, + { + `<img srcset="http://godoc.org/ 2x, https://golang.org/ {{.}}">`, + []string{ + `#ZgotmplZ`, + `#ZgotmplZ`, + `Hello,#ZgotmplZ`, + ` dir=%22ltr%22`, + `#ZgotmplZ, World!%22%29;`, + `Hello,#ZgotmplZ`, + `greeting=H%69%2c&addressee=%28World%29`, + `greeting=H%69,&addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`, + `%2cfoo/%2c`, }, }, } diff --git a/libgo/go/html/template/context.go b/libgo/go/html/template/context.go index 37a3faf..50730d3 100644 --- a/libgo/go/html/template/context.go +++ b/libgo/go/html/template/context.go @@ -102,6 +102,8 @@ const ( stateAttr // stateURL occurs inside an HTML attribute whose content is a URL. stateURL + // stateSrcset occurs inside an HTML srcset attribute. + stateSrcset // stateJS occurs inside an event handler or script element. stateJS // stateJSDqStr occurs inside a JavaScript double quoted string. @@ -145,6 +147,7 @@ var stateNames = [...]string{ stateRCDATA: "stateRCDATA", stateAttr: "stateAttr", stateURL: "stateURL", + stateSrcset: "stateSrcset", stateJS: "stateJS", stateJSDqStr: "stateJSDqStr", stateJSSqStr: "stateJSSqStr", @@ -326,6 +329,8 @@ const ( attrStyle // attrURL corresponds to an attribute whose value is a URL. attrURL + // attrSrcset corresponds to a srcset attribute. + attrSrcset ) var attrNames = [...]string{ @@ -334,6 +339,7 @@ var attrNames = [...]string{ attrScriptType: "attrScriptType", attrStyle: "attrStyle", attrURL: "attrURL", + attrSrcset: "attrSrcset", } func (a attr) String() string { diff --git a/libgo/go/html/template/escape.go b/libgo/go/html/template/escape.go index b51a370..5963194 100644 --- a/libgo/go/html/template/escape.go +++ b/libgo/go/html/template/escape.go @@ -71,6 +71,7 @@ var funcMap = template.FuncMap{ "_html_template_jsvalescaper": jsValEscaper, "_html_template_nospaceescaper": htmlNospaceEscaper, "_html_template_rcdataescaper": rcdataEscaper, + "_html_template_srcsetescaper": srcsetFilterAndEscaper, "_html_template_urlescaper": urlEscaper, "_html_template_urlfilter": urlFilter, "_html_template_urlnormalizer": urlNormalizer, @@ -215,6 +216,8 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context { case stateAttrName, stateTag: c.state = stateAttrName s = append(s, "_html_template_htmlnamefilter") + case stateSrcset: + s = append(s, "_html_template_srcsetescaper") default: if isComment(c.state) { s = append(s, "_html_template_commentescaper") @@ -280,9 +283,22 @@ func ensurePipelineContains(p *parse.PipeNode, s []string) { } // Rewrite the pipeline, creating the escapers in s at the end of the pipeline. newCmds := make([]*parse.CommandNode, pipelineLen, pipelineLen+len(s)) - copy(newCmds, p.Cmds) + insertedIdents := make(map[string]bool) + for i := 0; i < pipelineLen; i++ { + cmd := p.Cmds[i] + newCmds[i] = cmd + if idNode, ok := cmd.Args[0].(*parse.IdentifierNode); ok { + insertedIdents[normalizeEscFn(idNode.Ident)] = true + } + } for _, name := range s { - newCmds = appendCmd(newCmds, newIdentCmd(name, p.Position())) + if !insertedIdents[normalizeEscFn(name)] { + // When two templates share an underlying parse tree via the use of + // AddParseTree and one template is executed after the other, this check + // ensures that escapers that were already inserted into the pipeline on + // the first escaping pass do not get inserted again. + newCmds = appendCmd(newCmds, newIdentCmd(name, p.Position())) + } } p.Cmds = newCmds } @@ -317,13 +333,16 @@ var equivEscapers = map[string]string{ // 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 normalizeEscFn(a) == normalizeEscFn(b) +} + +// normalizeEscFn(a) is equal to normalizeEscFn(b) for any pair of names of +// escaper functions a and b that are equivalent. +func normalizeEscFn(e string) string { + if norm := equivEscapers[e]; norm != "" { + return norm } - return a == b + return e } // redundantFuncs[a][b] implies that funcMap[b](funcMap[a](x)) == funcMap[a](x) diff --git a/libgo/go/html/template/escape_test.go b/libgo/go/html/template/escape_test.go index 92f12ca..55f808c 100644 --- a/libgo/go/html/template/escape_test.go +++ b/libgo/go/html/template/escape_test.go @@ -650,6 +650,12 @@ func TestEscape(t *testing.T) { `<{{"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">`, + }, } for _, test := range tests { @@ -1840,7 +1846,7 @@ func TestErrorOnUndefined(t *testing.T) { err := tmpl.Execute(nil, nil) if err == nil { - t.Fatal("expected error") + t.Error("expected error") } if !strings.Contains(err.Error(), "incomplete") { t.Errorf("expected error about incomplete template; got %s", err) @@ -1860,10 +1866,10 @@ func TestIdempotentExecute(t *testing.T) { for i := 0; i < 2; i++ { err = tmpl.ExecuteTemplate(got, "hello", nil) if err != nil { - t.Fatalf("unexpected error: %s", err) + t.Errorf("unexpected error: %s", err) } if got.String() != want { - t.Fatalf("after executing template \"hello\", got:\n\t%q\nwant:\n\t%q\n", got.String(), want) + t.Errorf("after executing template \"hello\", got:\n\t%q\nwant:\n\t%q\n", got.String(), want) } got.Reset() } @@ -1871,7 +1877,7 @@ func TestIdempotentExecute(t *testing.T) { // "main" does not cause the output of "hello" to change. err = tmpl.ExecuteTemplate(got, "main", nil) if err != nil { - t.Fatalf("unexpected error: %s", err) + 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;". @@ -1881,19 +1887,6 @@ func TestIdempotentExecute(t *testing.T) { } } -// This covers issue #21844. -func TestAddExistingTreeError(t *testing.T) { - tmpl := Must(New("foo").Parse(`<p>{{.}}</p>`)) - tmpl, err := tmpl.AddParseTree("bar", tmpl.Tree) - if err == nil { - t.Fatalf("expected error after AddParseTree") - } - const want = `html/template: cannot add parse tree that template "foo" already references` - if got := err.Error(); got != want { - t.Errorf("got error:\n\t%q\nwant:\n\t%q\n", got, want) - } -} - func BenchmarkEscapedExecute(b *testing.B) { tmpl := Must(New("t").Parse(`<a onclick="alert('{{.}}')">{{.}}</a>`)) var buf bytes.Buffer @@ -1903,3 +1896,53 @@ func BenchmarkEscapedExecute(b *testing.B) { 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`)) + + var b bytes.Buffer + 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) + } + var b1, b2 bytes.Buffer + 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) + } +} diff --git a/libgo/go/html/template/template.go b/libgo/go/html/template/template.go index d77aa3d..4641a37 100644 --- a/libgo/go/html/template/template.go +++ b/libgo/go/html/template/template.go @@ -219,11 +219,6 @@ func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error t.nameSpace.mu.Lock() defer t.nameSpace.mu.Unlock() - for _, tmpl := range t.set { - if tmpl.Tree == tree { - return nil, fmt.Errorf("html/template: cannot add parse tree that template %q already references", tmpl.Name()) - } - } text, err := t.text.AddParseTree(name, tree) if err != nil { return nil, err @@ -300,6 +295,10 @@ func New(name string) *Template { // New allocates a new HTML template associated with the given one // and with the same delimiters. The association, which is transitive, // allows one template to invoke another with a {{template}} action. +// +// If a template with the given name already exists, the new HTML template +// will replace it. The existing template will be reset and disassociated with +// t. func (t *Template) New(name string) *Template { t.nameSpace.mu.Lock() defer t.nameSpace.mu.Unlock() @@ -314,6 +313,10 @@ func (t *Template) new(name string) *Template { nil, t.nameSpace, } + if existing, ok := tmpl.set[name]; ok { + emptyTmpl := New(existing.Name()) + *existing = *emptyTmpl + } tmpl.set[name] = tmpl return tmpl } diff --git a/libgo/go/html/template/transition.go b/libgo/go/html/template/transition.go index df7ac22..c72cf1e 100644 --- a/libgo/go/html/template/transition.go +++ b/libgo/go/html/template/transition.go @@ -23,6 +23,7 @@ var transitionFunc = [...]func(context, []byte) (context, int){ stateRCDATA: tSpecialTagEnd, stateAttr: tAttr, stateURL: tURL, + stateSrcset: tURL, stateJS: tJS, stateJSDqStr: tJSDelimited, stateJSSqStr: tJSDelimited, @@ -117,6 +118,8 @@ func tTag(c context, s []byte) (context, int) { attr = attrStyle case contentTypeJS: attr = attrScript + case contentTypeSrcset: + attr = attrSrcset } } @@ -161,6 +164,7 @@ var attrStartStates = [...]state{ attrScriptType: stateAttr, attrStyle: stateCSS, attrURL: stateURL, + attrSrcset: stateSrcset, } // tBeforeValue is the context transition function for stateBeforeValue. diff --git a/libgo/go/html/template/url.go b/libgo/go/html/template/url.go index a0bfe76..69a6ff4 100644 --- a/libgo/go/html/template/url.go +++ b/libgo/go/html/template/url.go @@ -37,13 +37,23 @@ func urlFilter(args ...interface{}) string { if t == contentTypeURL { return s } + if !isSafeUrl(s) { + return "#" + filterFailsafe + } + return s +} + +// isSafeUrl is true if s is a relative URL or if URL has a protocol in +// (http, https, mailto). +func isSafeUrl(s string) bool { if i := strings.IndexRune(s, ':'); i >= 0 && !strings.ContainsRune(s[:i], '/') { - protocol := strings.ToLower(s[:i]) - if protocol != "http" && protocol != "https" && protocol != "mailto" { - return "#" + filterFailsafe + + protocol := s[:i] + if !strings.EqualFold(protocol, "http") && !strings.EqualFold(protocol, "https") && !strings.EqualFold(protocol, "mailto") { + return false } } - return s + return true } // urlEscaper produces an output that can be embedded in a URL query. @@ -69,6 +79,16 @@ func urlProcessor(norm bool, args ...interface{}) string { norm = true } var b bytes.Buffer + if processUrlOnto(s, norm, &b) { + return b.String() + } + return s +} + +// processUrlOnto appends a normalized URL corresponding to its input to b +// and returns true if the appended content differs from s. +func processUrlOnto(s string, norm bool, b *bytes.Buffer) bool { + b.Grow(b.Cap() + len(s) + 16) written := 0 // The byte loop below assumes that all URLs use UTF-8 as the // content-encoding. This is similar to the URI to IRI encoding scheme @@ -114,12 +134,86 @@ func urlProcessor(norm bool, args ...interface{}) string { } } b.WriteString(s[written:i]) - fmt.Fprintf(&b, "%%%02x", c) + fmt.Fprintf(b, "%%%02x", c) written = i + 1 } - if written == 0 { + b.WriteString(s[written:]) + return written != 0 +} + +// Filters and normalizes srcset values which are comma separated +// URLs followed by metadata. +func srcsetFilterAndEscaper(args ...interface{}) string { + s, t := stringify(args...) + switch t { + case contentTypeSrcset: return s + case contentTypeURL: + // Normalizing gets rid of all HTML whitespace + // which separate the image URL from its metadata. + var b bytes.Buffer + if processUrlOnto(s, true, &b) { + s = b.String() + } + // Additionally, commas separate one source from another. + return strings.Replace(s, ",", "%2c", -1) } - b.WriteString(s[written:]) + + var b bytes.Buffer + written := 0 + for i := 0; i < len(s); i++ { + if s[i] == ',' { + filterSrcsetElement(s, written, i, &b) + b.WriteString(",") + written = i + 1 + } + } + filterSrcsetElement(s, written, len(s), &b) return b.String() } + +// Derived from https://play.golang.org/p/Dhmj7FORT5 +const htmlSpaceAndAsciiAlnumBytes = "\x00\x36\x00\x00\x01\x00\xff\x03\xfe\xff\xff\x07\xfe\xff\xff\x07" + +// isHtmlSpace is true iff c is a whitespace character per +// https://infra.spec.whatwg.org/#ascii-whitespace +func isHtmlSpace(c byte) bool { + return (c <= 0x20) && 0 != (htmlSpaceAndAsciiAlnumBytes[c>>3]&(1<<uint(c&0x7))) +} + +func isHtmlSpaceOrAsciiAlnum(c byte) bool { + return (c < 0x80) && 0 != (htmlSpaceAndAsciiAlnumBytes[c>>3]&(1<<uint(c&0x7))) +} + +func filterSrcsetElement(s string, left int, right int, b *bytes.Buffer) { + start := left + for start < right && isHtmlSpace(s[start]) { + start += 1 + } + end := right + for i := start; i < right; i++ { + if isHtmlSpace(s[i]) { + end = i + break + } + } + if url := s[start:end]; isSafeUrl(url) { + // If image metadata is only spaces or alnums then + // we don't need to URL normalize it. + metadataOk := true + for i := end; i < right; i++ { + if !isHtmlSpaceOrAsciiAlnum(s[i]) { + metadataOk = false + break + } + } + if metadataOk { + b.WriteString(s[left:start]) + processUrlOnto(url, true, b) + b.WriteString(s[end:right]) + return + } + } + b.WriteString("#") + b.WriteString(filterFailsafe) +} diff --git a/libgo/go/html/template/url_test.go b/libgo/go/html/template/url_test.go index 5182e9d..75c354e 100644 --- a/libgo/go/html/template/url_test.go +++ b/libgo/go/html/template/url_test.go @@ -87,6 +87,51 @@ func TestURLFilters(t *testing.T) { } } +func TestSrcsetFilter(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + "one ok", + "http://example.com/img.png", + "http://example.com/img.png", + }, + { + "one ok with metadata", + " /img.png 200w", + " /img.png 200w", + }, + { + "one bad", + "javascript:alert(1) 200w", + "#ZgotmplZ", + }, + { + "two ok", + "foo.png, bar.png", + "foo.png, bar.png", + }, + { + "left bad", + "javascript:alert(1), /foo.png", + "#ZgotmplZ, /foo.png", + }, + { + "right bad", + "/bogus#, javascript:alert(1)", + "/bogus#,#ZgotmplZ", + }, + } + + for _, test := range tests { + if got := srcsetFilterAndEscaper(test.input); got != test.want { + t.Errorf("%s: srcsetFilterAndEscaper(%q) want %q != %q", test.name, test.input, test.want, got) + } + } +} + func BenchmarkURLEscaper(b *testing.B) { for i := 0; i < b.N; i++ { urlEscaper("http://example.com:80/foo?q=bar%20&baz=x+y#frag") @@ -110,3 +155,15 @@ func BenchmarkURLNormalizerNoSpecials(b *testing.B) { urlNormalizer("http://example.com:80/foo?q=bar%20&baz=x+y#frag") } } + +func BenchmarkSrcsetFilter(b *testing.B) { + for i := 0; i < b.N; i++ { + srcsetFilterAndEscaper(" /foo/bar.png 200w, /baz/boo(1).png") + } +} + +func BenchmarkSrcsetFilterNoSpecials(b *testing.B) { + for i := 0; i < b.N; i++ { + srcsetFilterAndEscaper("http://example.com:80/foo?q=bar%20&baz=x+y#frag") + } +} |