plugins/viewhtml: add sanitizer struct
This commit is contained in:
parent
be3c069f5d
commit
c3e323161a
2 changed files with 26 additions and 16 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue