From 191a476823f19111e60af58ff5268756167ddc46 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 7 Apr 2024 13:34:14 +0200 Subject: [PATCH] fix(notifications): only sanitze html content in notifications, do not convert it to markdown Resolves https://community.vikunja.io/t/trello-import-html-mails/2197 --- go.mod | 11 +++++++---- go.sum | 14 ++++++++++++++ pkg/models/notifications.go | 26 ++++++++------------------ pkg/notifications/mail.go | 27 +++++++++++++++++++++++---- pkg/notifications/mail_render.go | 28 ++++++++++++++++++++++------ 5 files changed, 74 insertions(+), 32 deletions(-) diff --git a/go.mod b/go.mod index 8dd2ae06d..157602d63 100644 --- a/go.mod +++ b/go.mod @@ -67,12 +67,12 @@ require ( github.com/ulule/limiter/v3 v3.11.2 github.com/wneessen/go-mail v0.4.0 github.com/yuin/goldmark v1.7.0 - golang.org/x/crypto v0.21.0 + golang.org/x/crypto v0.22.0 golang.org/x/image v0.15.0 golang.org/x/oauth2 v0.18.0 golang.org/x/sync v0.6.0 - golang.org/x/sys v0.18.0 - golang.org/x/term v0.18.0 + golang.org/x/sys v0.19.0 + golang.org/x/term v0.19.0 golang.org/x/text v0.14.0 gopkg.in/d4l3k/messagediff.v1 v1.2.1 gopkg.in/yaml.v3 v3.0.1 @@ -90,6 +90,7 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/beevik/etree v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect @@ -121,6 +122,7 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -137,6 +139,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/microcosm-cc/bluemonday v1.0.26 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -176,7 +179,7 @@ require ( golang.org/x/arch v0.4.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.22.0 // indirect + golang.org/x/net v0.24.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.13.0 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index a4a2f0a33..c61eff8ff 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/arran4/golang-ical v0.2.7 h1:VO7YlVaGupZE15aj6NhUhte/MIfZuoIzkoI71VsG github.com/arran4/golang-ical v0.2.7/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bbrks/go-blurhash v1.1.1 h1:uoXOxRPDca9zHYabUTwvS4KnY++KKUbwFo+Yxb8ME4M= github.com/bbrks/go-blurhash v1.1.1/go.mod h1:lkAsdyXp+EhARcUo85yS2G1o+Sh43I2ebF5togC4bAY= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= @@ -215,6 +217,8 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -372,6 +376,8 @@ github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= +github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -575,6 +581,8 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -611,6 +619,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -654,6 +664,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -662,6 +674,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/pkg/models/notifications.go b/pkg/models/notifications.go index d86415b9f..6851bd3cc 100644 --- a/pkg/models/notifications.go +++ b/pkg/models/notifications.go @@ -17,10 +17,8 @@ package models import ( - "bufio" "sort" "strconv" - "strings" "time" "code.vikunja.io/api/pkg/utils" @@ -77,20 +75,16 @@ func (n *TaskCommentNotification) SubjectID() int64 { func (n *TaskCommentNotification) ToMail() *notifications.Mail { mail := notifications.NewMail(). - From(n.Doer.GetNameAndFromEmail()) + From(n.Doer.GetNameAndFromEmail()). + Subject("Re: " + n.Task.Title) - subject := "Re: " + n.Task.Title if n.Mentioned { - subject = n.Doer.GetName() + ` mentioned you in a comment in "` + n.Task.Title + `"` - mail.Line("**" + n.Doer.GetName() + "** mentioned you in a comment:") + mail. + Line("**" + n.Doer.GetName() + "** mentioned you in a comment:"). + Subject(n.Doer.GetName() + ` mentioned you in a comment in "` + n.Task.Title + `"`) } - mail.Subject(subject) - - lines := bufio.NewScanner(strings.NewReader(n.Comment.Comment)) - for lines.Scan() { - mail.Line(lines.Text()) - } + mail.HTML(n.Comment.Comment) return mail. Action("View Task", n.Task.GetFrontendURL()) @@ -306,12 +300,8 @@ func (n *UserMentionedInTaskNotification) ToMail() *notifications.Mail { mail := notifications.NewMail(). From(n.Doer.GetNameAndFromEmail()). Subject(subject). - Line("**" + n.Doer.GetName() + "** mentioned you in a task:") - - lines := bufio.NewScanner(strings.NewReader(n.Task.Description)) - for lines.Scan() { - mail.Line(lines.Text()) - } + Line("**" + n.Doer.GetName() + "** mentioned you in a task:"). + HTML(n.Task.Description) return mail. Action("View Task", n.Task.GetFrontendURL()) diff --git a/pkg/notifications/mail.go b/pkg/notifications/mail.go index bd9324cc2..f346313d1 100644 --- a/pkg/notifications/mail.go +++ b/pkg/notifications/mail.go @@ -26,8 +26,13 @@ type Mail struct { actionText string actionURL string greeting string - introLines []string - outroLines []string + introLines []*mailLine + outroLines []*mailLine +} + +type mailLine struct { + text string + isHTML bool } // NewMail creates a new mail object with a default greeting @@ -68,12 +73,26 @@ func (m *Mail) Action(text, url string) *Mail { // Line adds a line of text to the mail func (m *Mail) Line(line string) *Mail { + return m.appendLine(line, false) +} + +func (m *Mail) HTML(line string) *Mail { + return m.appendLine(line, true) +} + +func (m *Mail) appendLine(line string, isHTML bool) *Mail { if m.actionURL == "" { - m.introLines = append(m.introLines, line) + m.introLines = append(m.introLines, &mailLine{ + text: line, + isHTML: isHTML, + }) return m } - m.outroLines = append(m.outroLines, line) + m.outroLines = append(m.outroLines, &mailLine{ + text: line, + isHTML: isHTML, + }) return m } diff --git a/pkg/notifications/mail_render.go b/pkg/notifications/mail_render.go index 410056d8b..f06de283e 100644 --- a/pkg/notifications/mail_render.go +++ b/pkg/notifications/mail_render.go @@ -23,6 +23,8 @@ import ( templatehtml "html/template" templatetext "text/template" + "github.com/microcosm-cc/bluemonday" + "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/mail" "code.vikunja.io/api/pkg/utils" @@ -117,29 +119,43 @@ func RenderMail(m *Mail) (mailOpts *mail.Opts, err error) { data["Boundary"] = boundary data["FrontendURL"] = config.ServicePublicURL.GetString() + p := bluemonday.UGCPolicy() + var introLinesHTML []templatehtml.HTML for _, line := range m.introLines { - md := []byte(templatehtml.HTMLEscapeString(line)) + if line.isHTML { + // #nosec G203 -- the html is sanitized + introLinesHTML = append(introLinesHTML, templatehtml.HTML(p.Sanitize(line.text))) + continue + } + + md := []byte(templatehtml.HTMLEscapeString(line.text)) var buf bytes.Buffer err = goldmark.Convert(md, &buf) if err != nil { return nil, err } - //#nosec - the html is escaped few lines before - introLinesHTML = append(introLinesHTML, templatehtml.HTML(buf.String())) + // #nosec G203 -- the html is sanitized + introLinesHTML = append(introLinesHTML, templatehtml.HTML(p.Sanitize(buf.String()))) } data["IntroLinesHTML"] = introLinesHTML var outroLinesHTML []templatehtml.HTML for _, line := range m.outroLines { - md := []byte(templatehtml.HTMLEscapeString(line)) + if line.isHTML { + // #nosec G203 -- the html is sanitized + outroLinesHTML = append(outroLinesHTML, templatehtml.HTML(p.Sanitize(line.text))) + continue + } + + md := []byte(templatehtml.HTMLEscapeString(line.text)) var buf bytes.Buffer err = goldmark.Convert(md, &buf) if err != nil { return nil, err } - //#nosec - the html is escaped few lines before - outroLinesHTML = append(outroLinesHTML, templatehtml.HTML(buf.String())) + // #nosec G203 -- the html is sanitized + outroLinesHTML = append(outroLinesHTML, templatehtml.HTML(p.Sanitize(buf.String()))) } data["OutroLinesHTML"] = outroLinesHTML