all repos — grayfriday @ 2ce059289682484c7613f97608d525648ff9313f

blackfriday fork with a few changes

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 !isRelativeLink(val) && !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				if t == html.SelfClosingTagToken {
107					wr.WriteString("/>")
108				} else {
109					wr.WriteString(">")
110				}
111			} else {
112				wr.WriteString(html.EscapeString(string(tokenizer.Raw())))
113			}
114			// Make sure that tags like <script> that switch the parser into raw mode
115			// do not destroy the parse mode for following HTML text (the point is to
116			// escape them anyway). For that, switch off raw mode in the tokenizer.
117			tokenizer.NextIsNotRawText()
118		case html.EndTagToken:
119			// Whitelisted tokens can be written in raw.
120			tag, _ := tokenizer.TagName()
121			if whitelistTags[string(tag)] {
122				wr.Write(tokenizer.Raw())
123			} else {
124				wr.WriteString(html.EscapeString(string(tokenizer.Raw())))
125			}
126		case html.CommentToken:
127			// Comments are not really expected, but harmless.
128			wr.Write(tokenizer.Raw())
129		case html.DoctypeToken:
130			// Escape DOCTYPES, entities etc can be dangerous
131			wr.WriteString(html.EscapeString(string(tokenizer.Raw())))
132		default:
133			tokenizer.Token()
134			panic(fmt.Errorf("Unexpected token type %v", t))
135		}
136	}
137	err := tokenizer.Err()
138	if err != nil && err != io.EOF {
139		panic(tokenizer.Err())
140	}
141	wr.Flush()
142	return w.Bytes()
143}
144
145func protocolAllowed(attr []byte) bool {
146	for _, prefix := range whitelistProtocols {
147		if bytes.HasPrefix(attr, prefix) {
148			return true
149		}
150	}
151	return false
152}