MrRSS: Stored XSS via noscript in Article Content

Anyone who places HTML in a feed executes JavaScript in the MrRSS origin through a noscript element as soon as the victim opens the article.

Advisory ID: TP-2026-125
Product: MrRSS (cross-platform RSS reader with desktop and server mode)
Vulnerability type: Stored cross-site scripting (CWE-79)
CVE: not requested
CVSS 3.1: 8.1 (High) · CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N
Affected versions: <= 1.3.40
Vendor advisory: GHSA-8q87-xm7v-xv7q
Fixed in: 1.3.41
Reported: 22 October 2026

Summary

MrRSS is an RSS reader that passes feed-supplied article HTML first through a server-side allowlist filter and then through a client-side helper that adds lazy-loading hints. The filter removes active elements by name, and noscript is missing from that list. The Go parser treats the content of noscript as raw text that the attribute check never inspects, and the browser parses the same text back into markup during a template round trip. Anyone who places HTML in a feed item executes JavaScript in the application's origin as soon as the victim opens the article, on a stock install with one click and no account.

Root cause

PrepareArticleContent removes active elements through a name list that does not contain noscript (internal/utils/textutil/article_html.go:50). The Go parser used there runs with scripting enabled, so the children of a noscript element form a single text node that the attribute allowlist never checks and the serializer writes back unchanged (internal/utils/textutil/article_html.go:64-116). withLazyImages assigns that string to template.innerHTML and returns template.innerHTML, under a comment stating the content is already sanitized by the backend (frontend/src/utils/lazyImages.ts:11-30). A template's content belongs to a document without a browsing context, where parsing runs with scripting disabled and the children of noscript become real elements (frontend/src/utils/lazyImages.ts:11-30). ArticleContent.vue:179-181 passes the result to ArticleBody.vue:55, which renders it with v-html, while the test internal/utils/textutil/article_html_test.go:23-32 checks only the filter output for onerror.

Proof of Concept

<item><title>payload</title>
<description>&lt;p&gt;marker&lt;/p&gt;&lt;p&gt;&lt;noscript&gt;&lt;/p&gt;&lt;img src=x onerror="fetch('/api/settings').then(r=&gt;r.json()).then(j=&gt;document.documentElement.setAttribute('data-ghsa','keys='+Object.keys(j).length))"&gt;&lt;/noscript&gt;&lt;/p&gt;</description></item>

GET /api/articles/content?id=1
-> "<p>marker</p><p><noscript></p><img src=x onerror=\"...\"></noscript></p>"

rendered DOM after one click:
-> <p>marker</p><p><noscript></noscript></p><img src="x" onerror="..." loading="lazy" decoding="async">

The closing </p> before the <img> ends the element opened before noscript, so the template parser creates the image outside noscript. The loading and decoding attributes are written by withLazyImages and show that the executing node comes out of the round trip; a bare <img onerror> as a negative control is removed by the filter.

Impact

  • JavaScript execution in the application's origin, triggered by HTML in any feed the victim reads.
  • Read and write access to the unauthenticated local API, in the test all 147 settings keys, plus stored integration credentials, OPML export and the article store.
  • Server mode has no login, so the script acts with the authority of the instance owner.
  • Stock install, default view, one click, no account.

References

Is Something Like This in Your Software?

Our team found this vulnerability in the course of its work. Have your applications tested by the same specialists, with a penetration test from turingpoint.