plugins/viewhtml: add sanitizer struct

This commit is contained in:
Simon Ser 2020-02-25 09:51:57 +01:00
parent be3c069f5d
commit c3e323161a
No known key found for this signature in database
GPG key ID: 0FDE7BE0E88F5E48
2 changed files with 26 additions and 16 deletions

View file

@ -6,6 +6,7 @@ import (
"regexp" "regexp"
"strings" "strings"
koushinbase "git.sr.ht/~emersion/koushin/plugins/base"
"github.com/aymerick/douceur/css" "github.com/aymerick/douceur/css"
cssparser "github.com/chris-ramon/douceur/parser" cssparser "github.com/chris-ramon/douceur/parser"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
@ -68,7 +69,15 @@ var allowedStyles = map[string]bool{
"list-style-position": true, "list-style-position": true,
} }
func sanitizeCSSDecls(decls []*css.Declaration) []*css.Declaration { type sanitizer struct {
msg *koushinbase.IMAPMessage
}
func (san *sanitizer) sanitizeResourceURL(src string) string {
return "about:blank"
}
func (san *sanitizer) sanitizeCSSDecls(decls []*css.Declaration) []*css.Declaration {
sanitized := make([]*css.Declaration, 0, len(decls)) sanitized := make([]*css.Declaration, 0, len(decls))
for _, decl := range decls { for _, decl := range decls {
if !allowedStyles[decl.Property] { if !allowedStyles[decl.Property] {
@ -86,26 +95,26 @@ func sanitizeCSSDecls(decls []*css.Declaration) []*css.Declaration {
return sanitized return sanitized
} }
func sanitizeCSSRule(rule *css.Rule) { func (san *sanitizer) sanitizeCSSRule(rule *css.Rule) {
// Disallow @import // Disallow @import
if rule.Kind == css.AtRule && strings.EqualFold(rule.Name, "@import") { if rule.Kind == css.AtRule && strings.EqualFold(rule.Name, "@import") {
rule.Prelude = "url(about:blank)" rule.Prelude = "url(about:blank)"
} }
rule.Declarations = sanitizeCSSDecls(rule.Declarations) rule.Declarations = san.sanitizeCSSDecls(rule.Declarations)
for _, child := range rule.Rules { for _, child := range rule.Rules {
sanitizeCSSRule(child) san.sanitizeCSSRule(child)
} }
} }
func sanitizeNode(n *html.Node) { func (san *sanitizer) sanitizeNode(n *html.Node) {
if n.Type == html.ElementNode { if n.Type == html.ElementNode {
if strings.EqualFold(n.Data, "img") { if strings.EqualFold(n.Data, "img") {
for i := range n.Attr { for i := range n.Attr {
attr := &n.Attr[i] attr := &n.Attr[i]
if strings.EqualFold(attr.Key, "src") { if strings.EqualFold(attr.Key, "src") {
attr.Val = "about:blank" attr.Val = san.sanitizeResourceURL(attr.Val)
} }
} }
} else if strings.EqualFold(n.Data, "style") { } else if strings.EqualFold(n.Data, "style") {
@ -126,7 +135,7 @@ func sanitizeNode(n *html.Node) {
s = "" s = ""
} else { } else {
for _, rule := range stylesheet.Rules { for _, rule := range stylesheet.Rules {
sanitizeCSSRule(rule) san.sanitizeCSSRule(rule)
} }
s = stylesheet.String() s = stylesheet.String()
@ -149,7 +158,7 @@ func sanitizeNode(n *html.Node) {
continue continue
} }
decls = sanitizeCSSDecls(decls) decls = san.sanitizeCSSDecls(decls)
attr.Val = "" attr.Val = ""
for _, d := range decls { for _, d := range decls {
@ -160,17 +169,17 @@ func sanitizeNode(n *html.Node) {
} }
for c := n.FirstChild; c != nil; c = c.NextSibling { for c := n.FirstChild; c != nil; c = c.NextSibling {
sanitizeNode(c) san.sanitizeNode(c)
} }
} }
func sanitizeHTML(b []byte) ([]byte, error) { func (san *sanitizer) sanitizeHTML(b []byte) ([]byte, error) {
doc, err := html.Parse(bytes.NewReader(b)) doc, err := html.Parse(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse HTML: %v", err) return nil, fmt.Errorf("failed to parse HTML: %v", err)
} }
sanitizeNode(doc) san.sanitizeNode(doc)
var buf bytes.Buffer var buf bytes.Buffer
if err := html.Render(&buf, doc); err != nil { if err := html.Render(&buf, doc); err != nil {

View file

@ -12,7 +12,7 @@ import (
"github.com/emersion/go-message" "github.com/emersion/go-message"
) )
const tpl = ` const tplSrc = `
<!-- allow-same-origin is required to resize the frame with its content --> <!-- allow-same-origin is required to resize the frame with its content -->
<!-- allow-popups is required for target="_blank" links --> <!-- allow-popups is required for target="_blank" links -->
<iframe id="email-frame" srcdoc="{{.}}" sandbox="allow-same-origin allow-popups"></iframe> <iframe id="email-frame" srcdoc="{{.}}" sandbox="allow-same-origin allow-popups"></iframe>
@ -20,6 +20,8 @@ const tpl = `
<link rel="stylesheet" href="/plugins/viewhtml/assets/style.css"> <link rel="stylesheet" href="/plugins/viewhtml/assets/style.css">
` `
var tpl = template.Must(template.New("view-html.html").Parse(tplSrc))
type viewer struct{} type viewer struct{}
func (viewer) ViewMessagePart(ctx *koushin.Context, msg *koushinbase.IMAPMessage, part *message.Entity) (interface{}, error) { func (viewer) ViewMessagePart(ctx *koushin.Context, msg *koushinbase.IMAPMessage, part *message.Entity) (interface{}, error) {
@ -36,15 +38,14 @@ func (viewer) ViewMessagePart(ctx *koushin.Context, msg *koushinbase.IMAPMessage
return nil, fmt.Errorf("failed to read part body: %v", err) return nil, fmt.Errorf("failed to read part body: %v", err)
} }
body, err = sanitizeHTML(body) san := sanitizer{msg}
body, err = san.sanitizeHTML(body)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to sanitize HTML part: %v", err) return nil, fmt.Errorf("failed to sanitize HTML part: %v", err)
} }
t := template.Must(template.New("view-html.html").Parse(tpl))
var buf bytes.Buffer var buf bytes.Buffer
err = t.Execute(&buf, string(body)) err = tpl.Execute(&buf, string(body))
if err != nil { if err != nil {
return nil, err return nil, err
} }