Commit 35d16226b9

35d16226b9a4b911c0abf3cf7305b28336028d84

parent: 91ac29cc4c

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-11 02:31 UTC

Escape everything rendered from user or DeviantArt input

Templates now use html/template, so query strings, usernames and titles
are escaped in context. The Go-built fragments (listings, comments,
descriptions, folders, tags, strips, user plates, pagination) escape
each DeviantArt-supplied string themselves and are handed to the
template as template.HTML. The Atom feed shares the same escaping.
ParseDescription's HTML branch emits whitelisted tags bare rather than
with their original attributes.

The 502 page shows only the first line of the upstream error, escaped,
instead of echoing a whole WAF block page.

Closes #13
Closes #19

Layout: unified · split

app/escape_test.go added +150
@@ -0,0 +1,150 @@
1package app
2
3import (
4 "html/template"
5 "net/http/httptest"
6 "skunkyart/static"
7 "strings"
8 "sync"
9 "testing"
10
11 "github.com/krazywarez/devianter"
12)
13
14var loadTemplatesOnce sync.Once
15
16// loadTemplates makes static.Templates usable from a test. The non-embed build
17// reads the repository's static/ directory; the embed build already has it.
18func loadTemplates() {
19 loadTemplatesOnce.Do(func() {
20 static.StaticPath = "../static"
21 static.CopyTemplatesToMemory()
22 })
23}
24
25// markup is the payload every escaping test injects. It closes an attribute,
26// closes a tag and opens a new element, which is what an injection needs to do.
27const markup = `"><b id=injected>x</b>`
28
29// TestEveryPageTemplateRenders pins down that the switch to html/template
30// parses and executes every page: html/template rejects some constructs
31// text/template accepts, and a failure here would be a 500 on every request.
32func TestEveryPageTemplateRenders(t *testing.T) {
33 loadTemplates()
34 for _, page := range []string{"about.htm", "daily.htm", "deviantion.htm", "gruser.htm", "search.htm"} {
35 rec := httptest.NewRecorder()
36 s := skunkyart{Writer: rec, Host: "http://localhost", BasePath: "/"}
37 s.ExecuteTemplate(page, "html", &s)
38 if rec.Code != 200 || !strings.Contains(rec.Body.String(), "</html>") {
39 t.Errorf("%s: status %d, body %q", page, rec.Code, rec.Body.String())
40 }
41 }
42
43 rec := httptest.NewRecorder()
44 uri := "/"
45 skunkyart{Writer: rec}.ExecuteTemplate("index.htm", "html", &uri)
46 if rec.Code != 200 || !strings.Contains(rec.Body.String(), "</html>") {
47 t.Errorf("index.htm: status %d, body %q", rec.Code, rec.Body.String())
48 }
49}
50
51// TestSearchPageEscapesTheQuery is the regression test for the reflected
52// query: it appears in the search box's value attribute and in the results
53// heading, and both must show it as text.
54func TestSearchPageEscapesTheQuery(t *testing.T) {
55 loadTemplates()
56 rec := httptest.NewRecorder()
57 s := skunkyart{Writer: rec, Host: "http://localhost", BasePath: "/", Endpoint: "search", QueryRaw: markup}
58 s.Templates.Search.List = template.HTML("<div></div>")
59 s.Templates.Search.Content.Total = 1
60 s.ExecuteTemplate("search.htm", "html", &s)
61
62 body := rec.Body.String()
63 if strings.Contains(body, "<b id=injected>") {
64 t.Fatalf("query rendered as markup:\n%s", body)
65 }
66 if n := strings.Count(body, "&lt;b id=injected&gt;"); n != 3 {
67 t.Errorf("escaped query appears %d times, want 3 (title, value attribute, heading):\n%s", n, body)
68 }
69}
70
71// TestDeviationListEscapesTitles covers the Go-built listing, which
72// html/template cannot escape because it arrives as template.HTML.
73func TestDeviationListEscapesTitles(t *testing.T) {
74 nsfw := CFG.Nsfw
75 CFG.Nsfw = true
76 defer func() { CFG.Nsfw = nsfw }()
77
78 d := devianter.Deviation{Title: markup}
79 d.Author.Username = markup
80 devs := []devianter.Deviation{d}
81
82 out := skunkyart{Host: "http://localhost"}.DeviationList(devs, false)
83 if strings.Contains(out, "<b id=injected>") || !strings.Contains(out, "&lt;b id=injected&gt;") {
84 t.Errorf("HTML listing did not escape the title:\n%s", out)
85 }
86
87 rec := httptest.NewRecorder()
88 skunkyart{Host: "http://localhost", Writer: rec, Atom: true}.DeviationList(devs, true)
89 if feed := rec.Body.String(); strings.Contains(feed, "<b id=injected>") || !strings.Contains(feed, "&lt;b id=injected&gt;") {
90 t.Errorf("Atom feed did not escape the title:\n%s", feed)
91 }
92}
93
94// TestParseCommentsEscapesUsernames covers the comment thread, where the name
95// is written as link text and as the "In reply to" target.
96func TestParseCommentsEscapesUsernames(t *testing.T) {
97 var c devianter.Comments
98 var parent, reply devianter.Thread
99 parent.ID = 1
100 parent.User.Username = markup
101 reply.ID = 2
102 reply.Parent = 1
103 reply.User.Username = "bob"
104 c.Thread = []devianter.Thread{parent, reply}
105
106 out := skunkyart{Host: "http://localhost", _pth: "/post/x/y"}.ParseComments(c, devianter.Error{})
107 if strings.Contains(out, "<b id=injected>") {
108 t.Fatalf("username rendered as markup:\n%s", out)
109 }
110 if n := strings.Count(out, "&lt;b id=injected&gt;"); n != 4 {
111 t.Errorf("escaped username appears %d times, want 4 (avatar, link, author, reply target):\n%s", n, out)
112 }
113}
114
115// TestParseDescriptionEscapesMarkupText covers the plain-HTML branch: text is
116// escaped, whitelisted tags are kept bare, and anything else is dropped.
117func TestParseDescriptionEscapesMarkupText(t *testing.T) {
118 var d devianter.Text
119 d.Html.Markup = `a <b class="z">b</b> <script>alert(1)</script> &lt;i&gt;`
120
121 out := ParseDescription("http://localhost", d)
122 for _, bad := range []string{"<script>", `class="z"`, "<i>"} {
123 if strings.Contains(out, bad) {
124 t.Errorf("output contains %q:\n%s", bad, out)
125 }
126 }
127 for _, want := range []string{"<b>b</b>", "&lt;i&gt;"} {
128 if !strings.Contains(out, want) {
129 t.Errorf("output lacks %q:\n%s", want, out)
130 }
131 }
132}
133
134// TestErrorPageShowsOneEscapedLine covers the 502 page: a WAF block arrives
135// as a whole HTML document, and only its first line is echoed, as text.
136func TestErrorPageShowsOneEscapedLine(t *testing.T) {
137 rec := httptest.NewRecorder()
138 skunkyart{Writer: rec, Host: "http://localhost"}.Error(devianter.Error{Error: "blocked <!DOCTYPE html>\n<html>second line"})
139
140 body := rec.Body.String()
141 if rec.Code != 502 {
142 t.Errorf("status %d, want 502", rec.Code)
143 }
144 if strings.Contains(body, "second line") {
145 t.Errorf("error page carries lines past the first:\n%s", body)
146 }
147 if strings.Contains(body, "<!DOCTYPE html>") || !strings.Contains(body, "&lt;!DOCTYPE html&gt;") {
148 t.Errorf("upstream error not escaped:\n%s", body)
149 }
150}
app/parsers.go +43 −31
@@ -32,9 +32,9 @@ func (s skunkyart) ParseComments(c devianter.Comments, daError devianter.Error)
3232 cmmts.WriteString(`"><p id="`)
3333 cmmts.WriteString(strconv.Itoa(x.ID))
3434 cmmts.WriteString(`"><img src="`)
35 cmmts.WriteString(URLBuilder(s.Host, "media", "emojitar", x.User.Username, "?type=a"))
35 cmmts.WriteString(esc(URLBuilder(s.Host, "media", "emojitar", x.User.Username, "?type=a")))
3636 cmmts.WriteString(`" width="30px" height="30px"><a href="`)
37 cmmts.WriteString(URLBuilder(s.Host, "group_user", "?q=", x.User.Username, "&type=a"))
37 cmmts.WriteString(esc(URLBuilder(s.Host, "group_user", "?q=", x.User.Username, "&type=a")))
3838 cmmts.WriteString(`"><b`)
3939 cmmts.WriteString(` class="`)
4040 if x.User.Banned {
@@ -44,19 +44,19 @@ func (s skunkyart) ParseComments(c devianter.Comments, daError devianter.Error)
4444 cmmts.WriteString(`author`)
4545 }
4646 cmmts.WriteString(`">`)
47 cmmts.WriteString(x.User.Username)
47 cmmts.WriteString(esc(x.User.Username))
4848 cmmts.WriteString("</b></a> ")
4949
5050 if x.Parent > 0 {
5151 cmmts.WriteString(` In reply to <a href="`)
52 cmmts.WriteString(s._pth)
52 cmmts.WriteString(esc(s._pth))
5353 cmmts.WriteString("#")
5454 cmmts.WriteString(strconv.Itoa(x.Parent))
5555 cmmts.WriteString(`">`)
5656 if replied[x.Parent] == "" {
5757 cmmts.WriteString("???")
5858 } else {
59 cmmts.WriteString(replied[x.Parent])
59 cmmts.WriteString(esc(replied[x.Parent]))
6060 }
6161 cmmts.WriteString("</a>")
6262 }
@@ -108,27 +108,31 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con
108108 if !VisibleDeviation(data) {
109109 continue
110110 }
111 if preview, fullview := ParseMedia(s.Host, data.Media, 320), ParseMedia(s.Host, data.Media); true {
111 // Escaped once here: the same values go into both the HTML grid and the
112 // Atom feed, and html.EscapeString produces entities XML accepts too.
113 author, title := esc(data.Author.Username), esc(data.Title)
114 postURL := esc(ConvertDeviantArtURLToSkunkyArt(s.Host, data.Url))
115 if preview, fullview := esc(ParseMedia(s.Host, data.Media, 320)), esc(ParseMedia(s.Host, data.Media)); true {
112116 if allowAtom && s.Atom {
113117 s.Writer.Header().Add("Content-Type", "application/atom+xml")
114118 id := strconv.Itoa(data.ID)
115119 listContent.WriteString(`<entry><author><name>`)
116 listContent.WriteString(data.Author.Username)
120 listContent.WriteString(author)
117121 listContent.WriteString(`</name></author><title>`)
118 listContent.WriteString(data.Title)
122 listContent.WriteString(title)
119123 listContent.WriteString(`</title><link rel="alternate" type="text/html" href="`)
120 listContent.WriteString(URLBuilder(s.Host, "post", data.Author.Username, "atom-"+id))
124 listContent.WriteString(esc(URLBuilder(s.Host, "post", data.Author.Username, "atom-"+id)))
121125 listContent.WriteString(`"/><id>`)
122126 listContent.WriteString(id)
123127 listContent.WriteString(`</id><published>`)
124128 listContent.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
125129 listContent.WriteString(`</published>`)
126130 listContent.WriteString(`<media:group><media:title>`)
127 listContent.WriteString(data.Title)
131 listContent.WriteString(title)
128132 listContent.WriteString(`</media:title><media:thumbinal url="`)
129133 listContent.WriteString(preview)
130134 listContent.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`)
131 listContent.WriteString(ConvertDeviantArtURLToSkunkyArt(s.Host, data.Url))
135 listContent.WriteString(postURL)
132136 listContent.WriteString(`"><img src="`)
133137 listContent.WriteString(fullview)
134138 listContent.WriteString(`"/></a><p>`)
@@ -146,11 +150,11 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con
146150 listContent.WriteString(`<h1>[ TEXT ]</h1>`)
147151 }
148152 listContent.WriteString(`<br><a href="`)
149 listContent.WriteString(ConvertDeviantArtURLToSkunkyArt(s.Host, data.Url))
153 listContent.WriteString(postURL)
150154 listContent.WriteString(`">`)
151 listContent.WriteString(data.Author.Username)
155 listContent.WriteString(author)
152156 listContent.WriteString(" - ")
153 listContent.WriteString(data.Title)
157 listContent.WriteString(title)
154158
155159 if data.NSFW {
156160 listContent.WriteString(` [<span class="nsfw">NSFW</span>]`)
@@ -175,14 +179,14 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con
175179 case s.Type == 0:
176180 list.WriteString("Daily Deviations")
177181 case s.Type == 'g' && len(devs) != 0:
178 list.WriteString(devs[0].Author.Username)
182 list.WriteString(esc(devs[0].Author.Username))
179183 default:
180184 list.WriteString("SkunkyArt")
181185 }
182186 list.WriteString(`</title>`)
183187
184188 list.WriteString(`<link rel="alternate" href="`)
185 list.WriteString(s.Host)
189 list.WriteString(esc(s.Host))
186190 list.WriteString(`"/>`)
187191
188192 list.WriteString(listContent.String())
@@ -316,7 +320,7 @@ func ParseDescription(host string, dscr devianter.Text) string {
316320 for n := range Styles {
317321 Styles := &Styles[n]
318322 Styles.TxtRaw = x.Text[Styles.From:Styles.To]
319 Styles.Txt = TagBuilder(Styles.TxtRaw, tags[Styles.From*Styles.To]...)
323 Styles.Txt = TagBuilder(esc(Styles.TxtRaw), tags[Styles.From*Styles.To]...)
320324 }
321325 }
322326
@@ -325,13 +329,13 @@ func ParseDescription(host string, dscr devianter.Text) string {
325329 if len(x.EntityRanges) != 0 {
326330 d := entities[x.EntityRanges[0].Key]
327331 parsedDescription.WriteString(`<a href="`)
328 parsedDescription.WriteString(ConvertDeviantArtURLToSkunkyArt(host, d.Url))
332 parsedDescription.WriteString(esc(ConvertDeviantArtURLToSkunkyArt(host, d.Url)))
329333 parsedDescription.WriteString(`"><img width="50%" src="`)
330 parsedDescription.WriteString(ParseMedia(host, d.Media))
334 parsedDescription.WriteString(esc(ParseMedia(host, d.Media)))
331335 parsedDescription.WriteString(`" title="`)
332 parsedDescription.WriteString(d.Author.Username)
336 parsedDescription.WriteString(esc(d.Author.Username))
333337 parsedDescription.WriteString(" - ")
334 parsedDescription.WriteString(d.Title)
338 parsedDescription.WriteString(esc(d.Title))
335339 parsedDescription.WriteString(`"></a>`)
336340 }
337341 case "unstyled":
@@ -342,22 +346,22 @@ func ParseDescription(host string, dscr devianter.Text) string {
342346 tag = "h2"
343347 }
344348
345 parsedDescription.WriteString(x.Text[:r.From])
349 parsedDescription.WriteString(esc(x.Text[:r.From]))
346350 if len(urls) != 0 && len(x.EntityRanges) != 0 {
347351 ra := &x.EntityRanges[0]
348352
349353 parsedDescription.WriteString(`<a target="_blank" href="`)
350 parsedDescription.WriteString(urls[ra.Key])
354 parsedDescription.WriteString(esc(urls[ra.Key]))
351355 parsedDescription.WriteString(`">`)
352356 parsedDescription.WriteString(r.Txt)
353357 parsedDescription.WriteString(`</a>`)
354358 } else if l > n+1 {
355359 parsedDescription.WriteString(r.Txt)
356360 }
357 parsedDescription.WriteString(TagBuilder(tag, x.Text[r.To:]))
361 parsedDescription.WriteString(TagBuilder(tag, esc(x.Text[r.To:])))
358362 }
359363 } else {
360 parsedDescription.WriteString(x.Text)
364 parsedDescription.WriteString(esc(x.Text))
361365 }
362366 }
363367 parsedDescription.WriteString("<br>")
@@ -377,9 +381,9 @@ func ParseDescription(host string, dscr devianter.Text) string {
377381 if a.Key == "href" {
378382 url := DeleteTrackingFromURL(a.Val)
379383 parsedDescription.WriteString(`<a target="_blank" href="`)
380 parsedDescription.WriteString(url)
384 parsedDescription.WriteString(esc(url))
381385 parsedDescription.WriteString(`">`)
382 parsedDescription.WriteString(GetValueOfTag(tt))
386 parsedDescription.WriteString(esc(GetValueOfTag(tt)))
383387 parsedDescription.WriteString("</a> ")
384388 }
385389 }
@@ -397,20 +401,28 @@ func ParseDescription(host string, dscr devianter.Text) string {
397401 if title != "" {
398402 for x := -1; x < b; x++ {
399403 parsedDescription.WriteString(`<img src="`)
400 parsedDescription.WriteString(uri)
404 parsedDescription.WriteString(esc(uri))
401405 parsedDescription.WriteString(`" title="`)
402 parsedDescription.WriteString(title)
406 parsedDescription.WriteString(esc(title))
403407 parsedDescription.WriteString(`">`)
404408 }
405409 }
406410 }
407411 case "br", "li", "ul", "p", "b":
408 parsedDescription.WriteString(token.String())
412 // The bare tag, not token.String(): that would carry over
413 // whatever attributes DeviantArt's markup put on it.
414 if token.Type == html.EndTagToken {
415 parsedDescription.WriteString("</")
416 } else {
417 parsedDescription.WriteString("<")
418 }
419 parsedDescription.WriteString(token.Data)
420 parsedDescription.WriteString(">")
409421 case "div":
410422 parsedDescription.WriteString("<p> ")
411423 }
412424 case html.TextToken:
413 parsedDescription.Write(tt.Text())
425 parsedDescription.WriteString(esc(string(tt.Text())))
414426 }
415427 }
416428 }
app/util.go +40 −23
@@ -4,6 +4,8 @@ import (
44 "context"
55 "encoding/json"
66 "fmt"
7 htmlesc "html"
8 "html/template"
79 "io"
810 "net/http"
911 "net/url"
@@ -11,7 +13,6 @@ import (
1113 "skunkyart/static"
1214 "strconv"
1315 "strings"
14 "text/template"
1516 "time"
1617
1718 "github.com/krazywarez/devianter"
@@ -41,6 +42,14 @@ func tryWithExitStatus(err error, code int) {
4142 }
4243}
4344
45// esc escapes s for use as HTML text or inside a quoted attribute. The Go-built
46// fragments bypass html/template's contextual escaping because they are handed
47// to it as template.HTML, so every DeviantArt-supplied string they contain has
48// to be escaped here instead.
49func esc(s string) string {
50 return htmlesc.EscapeString(s)
51}
52
4453// restore swallows a panic in the calling goroutine so that one bad parse cannot
4554// take the whole process down. The panic is logged rather than dropped silently.
4655func restore() {
@@ -101,44 +110,48 @@ type skunkyart struct {
101110 API API
102111 Version string
103112
113 // The template.HTML fields hold fragments the Go builders already
114 // escaped, so html/template inserts them as-is. Everything typed string is
115 // escaped by the template at the point of use.
104116 Templates struct {
105117 About instanceAbout
106118
107 SomeList string
108 DDStrips string
119 SomeList template.HTML
120 DDStrips template.HTML
109121 Deviation struct {
110 Post devianter.Post
111 Related string
112 StringTime string
113 Tags string
114 Comments string
122 Post devianter.Post
123 Description template.HTML
124 Related template.HTML
125 StringTime string
126 Tags template.HTML
127 Comments template.HTML
115128 }
116129
117130 GroupUser struct {
118131 GR devianter.GRuser
119 Admins string
132 Admins template.HTML
120133 Group bool
121134 CreationDate string
122135
123136 About struct {
124137 A devianter.About
125138
126 DescriptionFormatted string
127 Interests, Social string
128 Comments string
139 DescriptionFormatted template.HTML
140 Interests, Social template.HTML
141 Comments template.HTML
129142 BG string
130143 BGMeta devianter.Deviation
131144 }
132145
133146 Gallery struct {
134 Folders string
147 Folders template.HTML
135148 Pages int
136 List string
149 List template.HTML
137150 }
138151 }
139152 Search struct {
140153 Content devianter.Search
141 List string
154 List template.HTML
142155 }
143156 }
144157}
@@ -182,15 +195,19 @@ func URLBuilder(host string, strs ...string) string {
182195 return str.String()
183196}
184197
185// Error responds 502 with the error DeviantArt reported upstream.
198// Error responds 502 with the error DeviantArt reported upstream. Only the
199// first line is shown: a WAF block arrives as a whole HTML page, which is
200// neither readable nor safe to echo.
186201func (s skunkyart) Error(dAerr devianter.Error) {
187202 s.Writer.WriteHeader(502)
188203
204 reason, _, _ := strings.Cut(dAerr.Error, "\n")
205
189206 var msg strings.Builder
190207 msg.WriteString(`<html><link rel="stylesheet" href="`)
191208 msg.WriteString(URLBuilder(s.Host, "stylesheet"))
192209 msg.WriteString(`" /><h3>DeviantArt error — '`)
193 msg.WriteString(dAerr.Error)
210 msg.WriteString(esc(reason))
194211 msg.WriteString("'</h3></html>")
195212
196213 wr(s.Writer, msg.String())
@@ -319,11 +336,11 @@ func ConvertDeviantArtURLToSkunkyArt(host, url string) (output string) {
319336func BuildUserPlate(host, name string) string {
320337 var htm strings.Builder
321338 htm.WriteString(`<div class="user-plate"><img src="`)
322 htm.WriteString(URLBuilder(host, "media", "emojitar", name, "?type=a"))
339 htm.WriteString(esc(URLBuilder(host, "media", "emojitar", name, "?type=a")))
323340 htm.WriteString(`"><a href="`)
324 htm.WriteString(URLBuilder(host, "group_user", "?type=about&q=", name))
341 htm.WriteString(esc(URLBuilder(host, "group_user", "?type=about&q=", name)))
325342 htm.WriteString(`">`)
326 htm.WriteString(name)
343 htm.WriteString(esc(name))
327344 htm.WriteString(`</a></div>`)
328345 return htm.String()
329346}
@@ -355,7 +372,7 @@ func (s skunkyart) NavBase(c DeviationList) string {
355372 prevrev := func(msg string, page int, onpage bool) {
356373 if !onpage {
357374 list.WriteString(`<a href="`)
358 list.WriteString(s._pth)
375 list.WriteString(esc(s._pth))
359376 list.WriteString(`?p=`)
360377 list.WriteString(strconv.Itoa(page))
361378 if s.Type != 0 {
@@ -364,11 +381,11 @@ func (s skunkyart) NavBase(c DeviationList) string {
364381 }
365382 if s.Query != "" {
366383 list.WriteString("&q=")
367 list.WriteString(s.Query)
384 list.WriteString(esc(s.Query))
368385 }
369386 if f := s.Args.Get("folder"); f != "" {
370387 list.WriteString("&folder=")
371 list.WriteString(f)
388 list.WriteString(esc(f))
372389 }
373390 list.WriteString(`">`)
374391 list.WriteString(msg)
app/wrapper.go +43 −40
@@ -1,6 +1,7 @@
11package app
22
33import (
4 "html/template"
45 "regexp"
56 "strconv"
67 "strings"
@@ -42,33 +43,33 @@ func (s skunkyart) GRUser() {
4243 var about = &x.ModuleData.GroupAbout
4344 group.Group = true
4445 group.CreationDate = x.ModuleData.GroupAbout.FoundatedAt.UTC().String()
45 group.About.DescriptionFormatted = ParseDescription(s.Host, about.Description)
46 group.About.DescriptionFormatted = template.HTML(ParseDescription(s.Host, about.Description)) //nolint:gosec // G203: ParseDescription escapes its input
4647 } else if false {
4748 group.About.A = x.ModuleData.About
4849 var about = &group.About.A
4950 group.CreationDate = time.Unix(time.Now().Unix()-x.ModuleData.About.RegDate, 0).UTC().String()
50 group.About.DescriptionFormatted = ParseDescription(s.Host, about.Description)
51 group.About.DescriptionFormatted = template.HTML(ParseDescription(s.Host, about.Description)) //nolint:gosec // G203: ParseDescription escapes its input
5152
5253 for _, val := range x.ModuleData.About.SocialLinks {
5354 var social strings.Builder
5455 social.WriteString(`<a target="_blank" href="`)
55 social.WriteString(val.Value)
56 social.WriteString(esc(val.Value))
5657 social.WriteString(`">`)
57 social.WriteString(val.Value)
58 social.WriteString(esc(val.Value))
5859 social.WriteString("</a><br>")
59 group.About.Social += social.String()
60 group.About.Social += template.HTML(social.String()) //nolint:gosec // G203: escaped above
6061 }
6162
6263 for _, val := range x.ModuleData.About.Interests {
6364 var interest strings.Builder
64 interest.WriteString(val.Label)
65 interest.WriteString(esc(val.Label))
6566 interest.WriteString(": <b>")
66 interest.WriteString(val.Value)
67 interest.WriteString(esc(val.Value))
6768 interest.WriteString("</b><br>")
68 group.About.Interests += interest.String()
69 group.About.Interests += template.HTML(interest.String()) //nolint:gosec // G203: escaped above
6970 }
7071 }
71 group.About.Comments = s.ParseComments(devianter.GetComments(strconv.Itoa(group.GR.Gruser.ID), "", s.Page, 4))
72 group.About.Comments = template.HTML(s.ParseComments(devianter.GetComments(strconv.Itoa(group.GR.Gruser.ID), "", s.Page, 4))) //nolint:gosec // G203: ParseComments escapes its input
7273
7374 case "cover_deviation":
7475 group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation
@@ -79,7 +80,7 @@ func (s skunkyart) GRUser() {
7980 for _, z := range x.ModuleData.GroupAdmins.Results {
8081 htm.WriteString(BuildUserPlate(s.Host, z.User.Username))
8182 }
82 group.Admins += htm.String()
83 group.Admins += template.HTML(htm.String()) //nolint:gosec // G203: BuildUserPlate escapes its input
8384 }
8485
8586 }
@@ -110,9 +111,9 @@ func (s skunkyart) GRUser() {
110111 }
111112
112113 if folderid > 0 || (s.Type == 'f' && all) {
113 group.Gallery.List = s.DeviationList(content.Content.Results, true, DeviationList{
114 group.Gallery.List = template.HTML(s.DeviationList(content.Content.Results, true, DeviationList{ //nolint:gosec // G203: DeviationList escapes its input
114115 More: content.Content.HasMore,
115 })
116 }))
116117 } else {
117118 for _, x := range content.Content.Gruser.Page.Modules {
118119 if len(x.ModuleData.Folders.Results) != 0 {
@@ -124,11 +125,11 @@ func (s skunkyart) GRUser() {
124125
125126 if !x.Thumb.NSFW || CFG.Nsfw {
126127 folders.WriteString(`<a href="`)
127 folders.WriteString(ConvertDeviantArtURLToSkunkyArt(s.Host, x.Thumb.Url))
128 folders.WriteString(esc(ConvertDeviantArtURLToSkunkyArt(s.Host, x.Thumb.Url)))
128129 folders.WriteString(`"><img loading="lazy" src="`)
129 folders.WriteString(ParseMedia(s.Host, x.Thumb.Media))
130 folders.WriteString(esc(ParseMedia(s.Host, x.Thumb.Media)))
130131 folders.WriteString(`" title="`)
131 folders.WriteString(x.Thumb.Title)
132 folders.WriteString(esc(x.Thumb.Title))
132133 folders.WriteString(`"></a>`)
133134 } else {
134135 folders.WriteString(`<h1>[ <span class="nsfw">NSFW</span> ]</h1>`)
@@ -138,25 +139,25 @@ func (s skunkyart) GRUser() {
138139 folders.WriteString(`<a href="group_user?folder=`)
139140 folders.WriteString(strconv.Itoa(x.FolderId))
140141 folders.WriteString("&q=")
141 folders.WriteString(s.Query)
142 folders.WriteString(esc(s.Query))
142143 folders.WriteString("&type=")
143144 folders.WriteRune(s.Type)
144145 folders.WriteString(`">`)
145 folders.WriteString(x.Name)
146 folders.WriteString(esc(x.Name))
146147 folders.WriteString(`</a>`)
147148
148149 folders.WriteString("</div>")
149150 }
150151 }
151152 folders.WriteString(`</div><h1 id="content"><a href="#content">#</a> Content</h1>`)
152 group.Gallery.Folders = folders.String()
153 group.Gallery.Folders = template.HTML(folders.String()) //nolint:gosec // G203: escaped above
153154 }
154155
155156 if x.Name == "folder_deviations" {
156 group.Gallery.List = s.DeviationList(x.ModuleData.Folder.Deviations, true, DeviationList{
157 group.Gallery.List = template.HTML(s.DeviationList(x.ModuleData.Folder.Deviations, true, DeviationList{ //nolint:gosec // G203: DeviationList escapes its input
157158 Pages: x.ModuleData.Folder.Pages,
158159 More: x.ModuleData.Folder.HasMore,
159 })
160 }))
160161 }
161162 }
162163 }
@@ -201,14 +202,14 @@ func (s skunkyart) Deviation(author, postname string) {
201202 }
202203
203204 if post.Post.Deviation.TextContent.Excerpt != "" {
204 post.Post.Description = ParseDescription(s.Host, post.Post.Deviation.TextContent)
205 post.Description = template.HTML(ParseDescription(s.Host, post.Post.Deviation.TextContent)) //nolint:gosec // G203: ParseDescription escapes its input
205206 } else {
206 post.Post.Description = ParseDescription(s.Host, post.Post.Deviation.Extended.DescriptionText)
207 post.Description = template.HTML(ParseDescription(s.Host, post.Post.Deviation.Extended.DescriptionText)) //nolint:gosec // G203: ParseDescription escapes its input
207208 }
208209
209210 for _, x := range post.Post.Deviation.Extended.RelatedContent {
210211 if len(x.Deviations) != 0 {
211 post.Related += s.DeviationList(x.Deviations, false)
212 post.Related += template.HTML(s.DeviationList(x.Deviations, false)) //nolint:gosec // G203: DeviationList escapes its input
212213 }
213214 }
214215
@@ -216,15 +217,15 @@ func (s skunkyart) Deviation(author, postname string) {
216217 for _, x := range post.Post.Deviation.Extended.Tags {
217218 var tag strings.Builder
218219 tag.WriteString(` <a href="`)
219 tag.WriteString(URLBuilder(s.Host, "search", "?q=", x.Name, "&type=tag"))
220 tag.WriteString(esc(URLBuilder(s.Host, "search", "?q=", x.Name, "&type=tag")))
220221 tag.WriteString(`">#`)
221 tag.WriteString(x.Name)
222 tag.WriteString(esc(x.Name))
222223 tag.WriteString("</a>")
223224
224 post.Tags += tag.String()
225 post.Tags += template.HTML(tag.String()) //nolint:gosec // G203: escaped above
225226 }
226227
227 post.Comments = s.ParseComments(devianter.GetComments(id, post.Post.Comments.Cursor, s.Page, 1))
228 post.Comments = template.HTML(s.ParseComments(devianter.GetComments(id, post.Post.Comments.Cursor, s.Page, 1))) //nolint:gosec // G203: ParseComments escapes its input
228229 post.StringTime = post.Post.Deviation.PublishedTime.UTC().String()
229230 post.Post.IMG = ParseMedia(s.Host, post.Post.Deviation.Media)
230231
@@ -241,20 +242,20 @@ func (s skunkyart) DD() {
241242 var strips strings.Builder
242243 for _, x := range dd.Strips {
243244 strips.WriteString(`<h3 class="`)
244 strips.WriteString(x.Codename)
245 strips.WriteString(esc(x.Codename))
245246 strips.WriteString(`"> <a href="#`)
246 strips.WriteString(x.Codename)
247 strips.WriteString(esc(x.Codename))
247248 strips.WriteString(`"># </a>`)
248 strips.WriteString(x.Title)
249 strips.WriteString(esc(x.Title))
249250 strips.WriteString(`</h3>`)
250251
251252 strips.WriteString(s.DeviationList(x.Deviations, false))
252253 }
253 s.Templates.DDStrips = strips.String()
254 s.Templates.SomeList = s.DeviationList(dd.Deviations, true, DeviationList{
254 s.Templates.DDStrips = template.HTML(strips.String()) //nolint:gosec // G203: escaped above
255 s.Templates.SomeList = template.HTML(s.DeviationList(dd.Deviations, true, DeviationList{ //nolint:gosec // G203: DeviationList escapes its input
255256 Pages: 0,
256257 More: dd.HasMore,
257 })
258 }))
258259 if !s.Atom {
259260 s.ExecuteTemplate("daily.htm", "html", &s)
260261 }
@@ -310,14 +311,16 @@ func (s skunkyart) Search() {
310311 }
311312
312313 if len(usernames) != 0 {
313 ss.List += `<div class="content plates">`
314 var plates strings.Builder
315 plates.WriteString(`<div class="content plates">`)
314316 for x := range len(usernames) {
315 ss.List += BuildUserPlate(s.Host, usernames[x])
317 plates.WriteString(BuildUserPlate(s.Host, usernames[x]))
316318 }
317 ss.List += `</div>`
318 ss.List += s.NavBase(DeviationList{
319 plates.WriteString(`</div>`)
320 plates.WriteString(s.NavBase(DeviationList{
319321 More: true,
320 })
322 }))
323 ss.List = template.HTML(plates.String()) //nolint:gosec // G203: BuildUserPlate escapes its input
321324 }
322325 default:
323326 s.ReturnHTTPError(400)
@@ -331,10 +334,10 @@ func (s skunkyart) Search() {
331334 return
332335 }
333336
334 ss.List = s.DeviationList(ss.Content.Results, false, DeviationList{
337 ss.List = template.HTML(s.DeviationList(ss.Content.Results, false, DeviationList{ //nolint:gosec // G203: DeviationList escapes its input
335338 Pages: ss.Content.Pages,
336339 More: ss.Content.HasMore,
337 })
340 }))
338341 }
339342
340343 s.ExecuteTemplate("search.htm", "html", &s)
static/html/deviantion.htm +2 −2
@@ -22,11 +22,11 @@
2222 <span>{{T "deviation.published"}}<strong>{{.Templates.Deviation.StringTime}}</strong>; Views: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Views}}</strong>; Favourites: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Favourites}}</strong>; Downloads: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Downloads}}</strong>
2323 <br><a target="_blank" href="https://www.deviantart.com/{{.Templates.Deviation.Post.Deviation.Author.Username}}/art/art-{{.Templates.Deviation.Post.Deviation.ID}}">{{T "deviation.original"}}</a>
2424 </span>
25 {{if (ne .Templates.Deviation.Post.Description "")}}
25 {{if (ne .Templates.Deviation.Description "")}}
2626 <figcaption>
2727 <details>
2828 <summary>{{T "deviation.description"}}</summary>
29 {{.Templates.Deviation.Post.Description}}
29 {{.Templates.Deviation.Description}}
3030 </details>
3131 </figcaption>
3232 {{end}}