Ech0's RSS feed renders unescaped tag names and raw-HTML markdown, stored XSS against subscribers
Date: 2026-05
Affected: go/github.com/lin-snow/Ech0
Attack vector: xss
Category: vulnerability-disclosure
Severity: Medium
CWE: CWE-116
CWE-79
CVSS: 4.8 (CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N)

## Summary The public RSS/Atom feed at `/rss` renders two attacker-controlled surfaces without HTML escaping. Tag names flow through `fmt.Appendf(renderedContent, "<br /><span class=\"tag\">#%s</span>", tag.Name)` at `internal/service/common/common.go:120`, and the Markdown renderer at `internal/util/md/md.go` does not set the `html.SkipHTML` flag, so raw HTML blocks in echo content pass through unmodified. The resulting Atom `<summary type="html">` is valid XML but contains executable `<script>` tags after the RSS reader decodes it. RSS subscribers whose readers render HTML (including many self-hosted and desktop clients) execute attacker JavaScript in the reader's origin. ## Details Tag sink at `internal/service/common/common.go:120`: ```go if len(msg.Tags) > 0 { for _, tag := range msg.Tags { renderedContent = fmt.Appendf(renderedContent, "<br /><span class=\"tag\">#%s</span>", tag.Name) } } ``` `fmt.Appendf` with `%s` does not HTML-escape. Tag names come from user-supplied `EchoUpsertDto.Tags` and are persisted after `strings.TrimSpace(strings.TrimPrefix(tag.Name, "#"))` at `internal/service/echo/echo.go:326`, which strips a leading `#` and trims whitespace but does nothing about HTML metacharacters. A tag name of `</span><script>document.title='RSS-XSS-HIT'</script><span>x` breaks out of the surrounding `<span>` element and injects executable JavaScript into the RSS `summary` field. Markdown sink at `internal/util/md/md.go`: ```go htmlF


Mitigations:

Architectural layers (MAESTRO):