sanitize.go (view raw)
1package blackfriday
2
3import (
4 "bufio"
5 "bytes"
6 "code.google.com/p/go.net/html"
7 "fmt"
8 "io"
9)
10
11// Whitelisted element tags, attributes on particular tags, attributes that are
12// interpreted as protocols (again on particular tags), and allowed protocols.
13var (
14 whitelistTags map[string]bool
15 whitelistAttrs map[string]map[string]bool
16 protocolAttrs map[string]map[string]bool
17 whitelistProtocols [][]byte
18)
19
20func init() {
21 whitelistTags = toSet([]string{
22 // Headings
23 "h1", "h2", "h3", "h4", "h5", "h6",
24 // Block elements
25 "p", "pre", "blockquote", "hr", "div", "header", "article", "aside", "footer",
26 "section", "main", "mark", "figure", "figcaption",
27 // Inline elements
28 "a", "br", "cite", "code", "img",
29 // Lists
30 "ol", "ul", "li",
31 // Tables
32 "table", "tbody", "td", "tfoot", "th", "thead", "tr", "colgroup", "col", "caption",
33 // Formatting
34 "u", "i", "em", "small", "strike", "b", "strong", "sub", "sup", "q",
35 // Definition lists
36 "dd", "dl", "dt",
37 })
38 whitelistAttrs = map[string]map[string]bool{
39 "a": toSet([]string{"href", "title", "rel"}),
40 "img": toSet([]string{"src", "alt", "title"}),
41 }
42 protocolAttrs = map[string]map[string]bool{
43 "a": toSet([]string{"href"}),
44 "img": toSet([]string{"src"}),
45 }
46 whitelistProtocols = [][]byte{
47 []byte("http://"),
48 []byte("https://"),
49 []byte("ftp://"),
50 []byte("mailto:"),
51 }
52}
53
54func toSet(keys []string) map[string]bool {
55 m := make(map[string]bool, len(keys))
56 for _, k := range keys {
57 m[k] = true
58 }
59 return m
60}
61
62// Sanitizes the given input by parsing it as HTML5, then whitelisting known to
63// be safe elements and attributes. All other HTML is escaped, unsafe attributes
64// are stripped.
65func sanitizeHtmlSafe(input []byte) []byte {
66 r := bytes.NewReader(input)
67 var w bytes.Buffer
68 tokenizer := html.NewTokenizer(r)
69 wr := bufio.NewWriter(&w)
70
71 // Iterate through all tokens in the input stream and sanitize them.
72 for t := tokenizer.Next(); t != html.ErrorToken; t = tokenizer.Next() {
73 switch t {
74 case html.TextToken:
75 // Text is written escaped.
76 wr.WriteString(tokenizer.Token().String())
77 case html.SelfClosingTagToken, html.StartTagToken:
78 // HTML tags are escaped unless whitelisted.
79 tag, hasAttributes := tokenizer.TagName()
80 tagName := string(tag)
81 if whitelistTags[tagName] {
82 wr.WriteString("<")
83 wr.Write(tag)
84 for hasAttributes {
85 var key, val []byte
86 key, val, hasAttributes = tokenizer.TagAttr()
87 attrName := string(key)
88 // Only include whitelisted attributes for the given tagName.
89 tagWhitelistedAttrs, ok := whitelistAttrs[tagName]
90 if ok && tagWhitelistedAttrs[attrName] {
91 // For whitelisted attributes, if it's an attribute that requires
92 // protocol checking, do so and strip it if it's not known to be safe.
93 tagProtocolAttrs, ok := protocolAttrs[tagName]
94 if ok && tagProtocolAttrs[attrName] {
95 if !protocolAllowed(val) {
96 continue
97 }
98 }
99 wr.WriteByte(' ')
100 wr.Write(key)
101 wr.WriteString(`="`)
102 wr.WriteString(html.EscapeString(string(val)))
103 wr.WriteByte('"')
104 }
105 }
106 wr.WriteString(">")
107 } else {
108 wr.WriteString(html.EscapeString(string(tokenizer.Raw())))
109 }
110 case html.EndTagToken:
111 // Whitelisted tokens can be written in raw.
112 tag, _ := tokenizer.TagName()
113 if whitelistTags[string(tag)] {
114 wr.Write(tokenizer.Raw())
115 } else {
116 wr.WriteString(html.EscapeString(string(tokenizer.Raw())))
117 }
118 case html.CommentToken:
119 // Comments are not really expected, but harmless.
120 wr.Write(tokenizer.Raw())
121 case html.DoctypeToken:
122 // Escape DOCTYPES, entities etc can be dangerous
123 wr.WriteString(html.EscapeString(string(tokenizer.Raw())))
124 default:
125 tokenizer.Token()
126 panic(fmt.Errorf("Unexpected token type %v", t))
127 }
128 }
129 err := tokenizer.Err()
130 if err != nil && err != io.EOF {
131 panic(tokenizer.Err())
132 }
133 wr.Flush()
134 return w.Bytes()
135}
136
137func protocolAllowed(attr []byte) bool {
138 for _, prefix := range whitelistProtocols {
139 if bytes.HasPrefix(attr, prefix) {
140 return true
141 }
142 }
143 return false
144}