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
`ZgotmplZ`,
`ZgotmplZ`,
`ZgotmplZ`,
+ `ZgotmplZ`,
+ `ZgotmplZ`,
},
},
{
`ZgotmplZ`,
`ZgotmplZ`,
`ZgotmplZ`,
+ `ZgotmplZ`,
+ `ZgotmplZ`,
},
},
{
` 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/,`,
},
},
{
`ZgotmplZ`,
`ZgotmplZ`,
`ZgotmplZ`,
+ `ZgotmplZ`,
+ `ZgotmplZ`,
},
},
{
` 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/,`,
},
},
{
` 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/,`,
},
},
{
` 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/,`,
},
},
{
`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/,"`,
},
},
{
`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/,"`,
},
},
{
`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\/,`,
},
},
{
`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\/,`,
},
},
{
`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/,"`,
},
},
{
` 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/,`,
},
},
{
`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\/,`,
},
},
{
`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/,`,
},
},
{
`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`,
},
},
}
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.
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
}
}
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)
+}