]> Cypherpunks repositories - gostls13.git/commitdiff
html/template: add srcset content type
authorMike Samuel <mikesamuel@gmail.com>
Sun, 19 Nov 2017 21:37:07 +0000 (06:37 +0900)
committerRuss Cox <rsc@golang.org>
Thu, 14 Dec 2017 19:54:38 +0000 (19:54 +0000)
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>

src/html/template/attr.go
src/html/template/content.go
src/html/template/content_test.go
src/html/template/context.go
src/html/template/escape.go
src/html/template/escape_test.go
src/html/template/transition.go
src/html/template/url.go
src/html/template/url_test.go

index 7438f51f6a9505addb42025fa3b2f087075b58cd..92d2789e8041e829264a5a641ecdf06e70c6920e 100644 (file)
@@ -120,6 +120,7 @@ var attrTypeMap = map[string]contentType{
        "src":         contentTypeURL,
        "srcdoc":      contentTypeHTML,
        "srclang":     contentTypePlain,
+       "srcset":      contentTypeSrcset,
        "start":       contentTypePlain,
        "step":        contentTypePlain,
        "style":       contentTypeCSS,
index 2e14bd1231f7997f6839892b0a43acbbb9a346e4..e7cdedc3b62099f1344bfcce79c411eb219082d8 100644 (file)
@@ -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 {
index 0b4365c83ba392d64c80f90e878ea68252e9366d..cc092f50c0c1e4df2f36a8f32fdcb40191b44633 100644 (file)
@@ -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=&#34;ltr&#34;`,
                                `c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
                                `Hello, World &amp; O&#39;Reilly\x21`,
-                               `greeting=H%69&amp;addressee=(World)`,
+                               `greeting=H%69,&amp;addressee=(World)`,
+                               `greeting=H%69,&amp;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) {
                                `&#32;dir&#61;&#34;ltr&#34;`,
                                `c&#32;&amp;&amp;&#32;alert(&#34;Hello,&#32;World!&#34;);`,
                                `Hello,&#32;World&#32;&amp;&#32;O&#39;Reilly\x21`,
-                               `greeting&#61;H%69&amp;addressee&#61;(World)`,
+                               `greeting&#61;H%69,&amp;addressee&#61;(World)`,
+                               `greeting&#61;H%69,&amp;addressee&#61;(World)&#32;2x,&#32;https://golang.org/favicon.ico&#32;500.5w`,
+                               `,foo/,`,
                        },
                },
                {
@@ -104,7 +116,9 @@ func TestTypedContent(t *testing.T) {
                                ` dir=&#34;ltr&#34;`,
                                `c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
                                `Hello, World &amp; O&#39;Reilly\x21`,
-                               `greeting=H%69&amp;addressee=(World)`,
+                               `greeting=H%69,&amp;addressee=(World)`,
+                               `greeting=H%69,&amp;addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
+                               `,foo/,`,
                        },
                },
                {
@@ -117,7 +131,9 @@ func TestTypedContent(t *testing.T) {
                                ` dir=&#34;ltr&#34;`,
                                `c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
                                `Hello, World &amp; O&#39;Reilly\x21`,
-                               `greeting=H%69&amp;addressee=(World)`,
+                               `greeting=H%69,&amp;addressee=(World)`,
+                               `greeting=H%69,&amp;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 &amp;&amp; alert(&#34;Hello, World!&#34;);`,
                                // Escape sequence not over-escaped.
                                `&#34;Hello, World &amp; O&#39;Reilly\x21&#34;`,
-                               `&#34;greeting=H%69\u0026addressee=(World)&#34;`,
+                               `&#34;greeting=H%69,\u0026addressee=(World)&#34;`,
+                               `&#34;greeting=H%69,\u0026addressee=(World) 2x, https://golang.org/favicon.ico 500.5w&#34;`,
+                               `&#34;,foo/,&#34;`,
                        },
                },
                {
@@ -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=&#34;ltr&#34;`,
                                `c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
                                `Hello, World &amp; O&#39;Reilly\x21`,
-                               `greeting=H%69&amp;addressee=(World)`,
+                               `greeting=H%69,&amp;addressee=(World)`,
+                               `greeting=H%69,&amp;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&amp;addressee=%28World%29`,
+                               `greeting=H%69,&amp;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&amp;addressee=%28World%29`,
+                               // Metadata is not escaped.
+                               `greeting=H%69,&amp;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
+                               `&#32;dir&#61;%22ltr%22`,
+                               `#ZgotmplZ,&#32;World!%22%29;`,
+                               `Hello,#ZgotmplZ`,
+                               `greeting&#61;H%69%2c&amp;addressee&#61;%28World%29`,
+                               `greeting&#61;H%69,&amp;addressee&#61;(World)&#32;2x,&#32;https://golang.org/favicon.ico&#32;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&amp;addressee=%28World%29`,
+                               `greeting=H%69,&amp;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&amp;addressee=%28World%29`,
+                               `greeting=H%69,&amp;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&amp;addressee=%28World%29`,
+                               `greeting=H%69,&amp;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&amp;addressee=%28World%29`,
+                               `greeting=H%69,&amp;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&amp;addressee=%28World%29`,
+                               `greeting=H%69,&amp;addressee=(World) 2x, https://golang.org/favicon.ico 500.5w`,
+                               `%2cfoo/%2c`,
                        },
                },
        }
index 37a3faf88b9c02ea40ef79664952c16fa6e03d22..50730d3f2b775d1676880ad6268038ab2a6dbcfd 100644 (file)
@@ -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 {
index b51a37039bde7d1f391135b95202bdd0fc2622fd..1241fa7713ccccf7cbf3a082aac3d50bf6e40245 100644 (file)
@@ -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")
index bd075661c6d17d019f28cd261b85491e0309c5a3..949985fe4a7dec1bfecf0bed457f6a5844a15edb 100644 (file)
@@ -650,6 +650,12 @@ func TestEscape(t *testing.T) {
                        `<{{"script"}}>{{"doEvil()"}}</{{"script"}}>`,
                        `&lt;script>doEvil()&lt;/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 {
index df7ac2289b456c4d4bc81d6a1ce39ca57b1f48bf..c72cf1ea60ef0a50f83802bdad161dbe826ba741 100644 (file)
@@ -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.
index a0bfe7672e31ccc71f630bc6f9d11ed7abdbada7..69a6ff49b844cd6714081c8f43fe1d1f4373eee2 100644 (file)
@@ -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)
+}
index 5182e9d794386a99cab2c77b346adf6b88674fa9..75c354eba87de41a8d4a8d4c4fc4fe8a0822d685 100644 (file)
@@ -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")
+       }
+}