mirror of https://github.com/golang/go.git
html/template: add srcset content type
Srcset is largely the same as a URL, but is escaped in URL contexts. Inside a srcset attribute, URLs have their commas percent-escaped to avoid having the URL be interpreted as multiple URLs. Srcset is placed in a srcset attribute literally. Fixes #17441 Change-Id: I676b544784c7e54954ddb91eeff242cab25d02c4 Reviewed-on: https://go-review.googlesource.com/38324 Reviewed-by: Kunpei Sakai <namusyaka@gmail.com> Reviewed-by: Mike Samuel <mikesamuel@gmail.com> Reviewed-by: Russ Cox <rsc@golang.org> Run-TryBot: Russ Cox <rsc@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org>
This commit is contained in:
parent
c7b7c43363
commit
c0cda71dab
|
|
@ -120,6 +120,7 @@ var attrTypeMap = map[string]contentType{
|
|||
"src": contentTypeURL,
|
||||
"srcdoc": contentTypeHTML,
|
||||
"srclang": contentTypePlain,
|
||||
"srcset": contentTypeSrcset,
|
||||
"start": contentTypePlain,
|
||||
"step": contentTypePlain,
|
||||
"style": contentTypeCSS,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -37,15 +37,25 @@ func urlFilter(args ...interface{}) string {
|
|||
if t == contentTypeURL {
|
||||
return s
|
||||
}
|
||||
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
|
||||
}
|
||||
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 := s[:i]
|
||||
if !strings.EqualFold(protocol, "http") && !strings.EqualFold(protocol, "https") && !strings.EqualFold(protocol, "mailto") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// urlEscaper produces an output that can be embedded in a URL query.
|
||||
// The output can be embedded in an HTML attribute without further escaping.
|
||||
func urlEscaper(args ...interface{}) string {
|
||||
|
|
@ -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 {
|
||||
return s
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue