rss email digests over ssh because you're a cool kid herald.dunkirk.sh
go rss rss-reader ssh charm
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add HTML sanitization for inline feed content

Implements safe HTML rendering for inline feeds by:
- Adding bluemonday library for HTML sanitization
- Creating sanitizeHTML function using UGCPolicy (allows safe tags, strips styles and unsafe elements)
- Introducing templateFeedItem and templateFeedGroup structs with SanitizedContent field
- Pre-sanitizing all feed content before template rendering
- Updating digest.html template to use SanitizedContent instead of raw Content

This prevents XSS attacks while still allowing safe HTML formatting like headings, links, lists, and basic text formatting in feed content.

Claude 3a44027a 6fe82b5e

+82 -4
+72 -3
email/render.go
··· 6 6 htmltemplate "html/template" 7 7 texttemplate "text/template" 8 8 "time" 9 + 10 + "github.com/microcosm-cc/bluemonday" 9 11 ) 10 12 11 13 //go:embed templates/* ··· 30 32 Published time.Time 31 33 } 32 34 35 + // templateFeedItem is used for template rendering with sanitized HTML content 36 + type templateFeedItem struct { 37 + Title string 38 + Link string 39 + Content string // Original content for text template 40 + SanitizedContent htmltemplate.HTML // Sanitized HTML for HTML template 41 + Published time.Time 42 + } 43 + 44 + // templateFeedGroup is used for template rendering with sanitized items 45 + type templateFeedGroup struct { 46 + FeedName string 47 + FeedURL string 48 + Items []templateFeedItem 49 + } 50 + 51 + // sanitizeHTML sanitizes HTML content, allowing safe tags while stripping styles and unsafe elements 52 + func sanitizeHTML(html string) string { 53 + return policy.Sanitize(html) 54 + } 55 + 33 56 var ( 34 57 htmlTmpl *htmltemplate.Template 35 58 textTmpl *texttemplate.Template 59 + policy *bluemonday.Policy 36 60 ) 37 61 38 62 func init() { ··· 45 69 if err != nil { 46 70 panic("failed to parse text template: " + err.Error()) 47 71 } 72 + 73 + // Initialize HTML sanitization policy 74 + // UGCPolicy allows safe HTML tags but strips styles and unsafe elements 75 + // This prevents XSS attacks while allowing basic formatting 76 + policy = bluemonday.UGCPolicy() 48 77 } 49 78 50 79 func RenderDigest(data *DigestData, inline bool, daysUntilExpiry int, showUrgentBanner, showWarningBanner bool) (html string, text string, err error) { 51 - tmplData := struct { 80 + // Convert FeedGroups to templateFeedGroups with sanitized HTML content 81 + sanitizedGroups := make([]templateFeedGroup, len(data.FeedGroups)) 82 + for i, group := range data.FeedGroups { 83 + sanitizedItems := make([]templateFeedItem, len(group.Items)) 84 + for j, item := range group.Items { 85 + sanitizedItems[j] = templateFeedItem{ 86 + Title: item.Title, 87 + Link: item.Link, 88 + Content: item.Content, 89 + SanitizedContent: htmltemplate.HTML(sanitizeHTML(item.Content)), 90 + Published: item.Published, 91 + } 92 + } 93 + sanitizedGroups[i] = templateFeedGroup{ 94 + FeedName: group.FeedName, 95 + FeedURL: group.FeedURL, 96 + Items: sanitizedItems, 97 + } 98 + } 99 + 100 + // Prepare template data for HTML template (with sanitized content) 101 + htmlTmplData := struct { 102 + ConfigName string 103 + TotalItems int 104 + FeedGroups []templateFeedGroup 105 + Inline bool 106 + DaysUntilExpiry int 107 + ShowUrgentBanner bool 108 + ShowWarningBanner bool 109 + }{ 110 + ConfigName: data.ConfigName, 111 + TotalItems: data.TotalItems, 112 + FeedGroups: sanitizedGroups, 113 + Inline: inline, 114 + DaysUntilExpiry: daysUntilExpiry, 115 + ShowUrgentBanner: showUrgentBanner, 116 + ShowWarningBanner: showWarningBanner, 117 + } 118 + 119 + // Prepare template data for text template (with original content) 120 + textTmplData := struct { 52 121 *DigestData 53 122 Inline bool 54 123 DaysUntilExpiry int ··· 64 133 65 134 var htmlBuf, textBuf bytes.Buffer 66 135 67 - if err = htmlTmpl.Execute(&htmlBuf, tmplData); err != nil { 136 + if err = htmlTmpl.Execute(&htmlBuf, htmlTmplData); err != nil { 68 137 return "", "", err 69 138 } 70 139 71 - if err = textTmpl.Execute(&textBuf, tmplData); err != nil { 140 + if err = textTmpl.Execute(&textBuf, textTmplData); err != nil { 72 141 return "", "", err 73 142 } 74 143
+1 -1
email/templates/digest.html
··· 67 67 {{range .Items}} 68 68 <div> 69 69 <h1><a href="{{.Link}}">{{.Title}}</a></h1> 70 - <div>{{.Content}}</div> 70 + <div>{{.SanitizedContent}}</div> 71 71 </div> 72 72 <hr /> 73 73 {{end}}
+3
go.mod
··· 27 27 github.com/andybalholm/cascadia v1.3.3 // indirect 28 28 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 29 29 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 30 + github.com/aymerick/douceur v0.2.0 // indirect 30 31 github.com/charmbracelet/bubbletea v1.3.10 // indirect 31 32 github.com/charmbracelet/colorprofile v0.4.1 // indirect 32 33 github.com/charmbracelet/keygen v0.5.4 // indirect ··· 44 45 github.com/creack/pty v1.1.24 // indirect 45 46 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 46 47 github.com/go-logfmt/logfmt v0.6.1 // indirect 48 + github.com/gorilla/css v1.0.1 // indirect 47 49 github.com/inconshreveable/mousetrap v1.1.0 // indirect 48 50 github.com/json-iterator/go v1.1.12 // indirect 49 51 github.com/kr/fs v0.1.0 // indirect ··· 51 53 github.com/mattn/go-isatty v0.0.20 // indirect 52 54 github.com/mattn/go-localereader v0.0.1 // indirect 53 55 github.com/mattn/go-runewidth v0.0.19 // indirect 56 + github.com/microcosm-cc/bluemonday v1.0.27 // indirect 54 57 github.com/mmcdole/goxpp v1.1.1 // indirect 55 58 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 56 59 github.com/modern-go/reflect2 v1.0.2 // indirect
+6
go.sum
··· 12 12 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 13 13 github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= 14 14 github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= 15 + github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 16 + github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 15 17 github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= 16 18 github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= 17 19 github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= ··· 68 70 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 69 71 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 70 72 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 73 + github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= 74 + github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 71 75 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 72 76 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 73 77 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= ··· 88 92 github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= 89 93 github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= 90 94 github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 95 + github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 96 + github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 91 97 github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4= 92 98 github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE= 93 99 github.com/mmcdole/goxpp v1.1.1 h1:RGIX+D6iQRIunGHrKqnA2+700XMCnNv0bAOOv5MUhx8=