CVE-2026-29038: changedetection.io: Reflected XSS in RSS Tag Error Response
A reflected cross-site scripting (XSS) vulnerability was identified in the /rss/tag/ endpoint of changedetection.io. The taguuid path parameter is reflected directly in the HTTP response body without HTML escaping. Since Flask returns text/html by default for plain string responses, the browser parses and executes injected JavaScript.
This vulnerability persists in version 0.54.1, which patched the related XSS in /rss/watch/ (CVE-2026-27645 / GHSA-mw8m-398g-h89w) but did not address the identical pattern in the tag RSS endpoint.
Package
- Ecosystem: pip - Package: changedetection.io - Affected versions: <= 0.54.1 - Patched versions: (none yet)
Severity Moderate - CVSS 6.1 CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Details File: changedetectionio/blueprint/rss/tag.py Line: 36 Source: tag.py @ 1d72716
The taguuid parameter from the URL path is interpolated into the response body using an f-string with no escaping:
python tag = datastore.data['settings']['application'].get('tags', {}).get(taguuid) if not tag: return f"Tag with UUID {taguuid} not found", 404 # ← No escaping, Content-Type: text/html
Flask's default Content-Type for plain string responses is text/html; charset=utf-8, so any HTML/JavaScript injected via {taguuid} is rendered and executed by the browser.
Relationship to CVE-2026-27645
CVE-2026-27645 (GHSA-mw8m-398g-h89w) addressed the identical vulnerability pattern in /rss/watch/ (singlewatch.py). The fix applied in v0.54.1 patched that endpoint but did not fix the same pattern in /rss/tag/ (tag.py). Testing confirms:
- /rss/watch/ on v0.54.1 — Returns generic 404 page, XSS no longer triggers ✅ - /rss/tag/ on v0.54.1 — XSS payload still fires, vulnerability confirmed ❌
Attack Vector
The attack requires a valid RSS access token, which is a 32-character hex string exposed in the <link> HTML tag on the homepage without authentication:
1. Attacker visits the target's homepage (if unauthenticated) and extracts the RSS token from the <link> tag 2. Crafts a malicious URL: http://target:5000/rss/tag/<img src=x onerror=alert(document.cookie)>?token=EXTRACTEDTOKEN 3. Sends the link to a victim who has an active session on the changedetection.io instance 4. When the victim clicks the link, the server responds with: Tag with UUID <img src=x onerror=alert(document.cookie)> not found 5. The browser renders the <img> tag, the onerror fires, and JavaScript executes in the victim's session context
Proof of Concept
Request
http GET /rss/tag/%3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3E?token=60b83b06df98b24c66367bc3d233105b HTTP/1.1 Host: localhost:5000
Response
http HTTP/1.1 404 NOT FOUND Content-Type: text/html; charset=utf-8
Tag with UUID <img src=x onerror=alert(document.domain)> not found
The XSS payload is reflected unescaped in an HTML response. The browser executes alert(document.domain) and displays "localhost", confirming JavaScript execution.
Tested on: changedetection.io v0.54.1 (Docker, localhost, Feb 25, 2026)
https://github.com/user-attachments/assets/6db07f6a-6df8-48a7-a597-9f39dfa1bb29
Impact
- Session cookie theft via document.cookie exfiltration - Account takeover if session cookies lack the HttpOnly flag - Phishing via crafted links that appear to originate from a trusted changedetection.io instance - Low exploitation barrier - the RSS token is obtainable without authentication from the homepage <link> tag - Widespread exposure - prior scanning of internet-facing instances (during CVE-2026-27645 research) identified 500+ publicly accessible deployments
Suggested Fix
Escape the taguuid parameter before reflecting it in the response, or set the Content-Type to text/plain:
Option A: HTML Escape (Recommended)
python from markupsafe import escape
if not tag: return f"Tag with UUID {escape(taguuid)} not found", 404
Option B: Set Content-Type to text/plain
python from flask import makeresponse
if not tag: resp = makeresponse(f"Tag with UUID {taguuid} not found", 404) resp.headers['Content-Type'] = 'text/plain; charset=utf-8' return resp
Credits
- Roberto Nunes (@Akokonunes) - Reporter - neo-ai-engineer (@neo-ai-engineer) - Reporter
References - Related advisory: GHSA-mw8m-398g-h89w (CVE-2026-27645) - Vulnerable source: tag.py @ 1d72716
Other sources
changedetection.io is a free open source web page change detection tool. Prior to version 0.54.4, there is a reflected cross-site scripting (XSS) vulnerability identified in the /rss/tag/ endpoint of changedetection.io. The taguuid path parameter is reflected directly in the HTTP response body without HTML escaping. Since Flask returns text/html by default for plain string responses, the browser parses and executes injected JavaScript. This issue has been patched in version 0.54.4.
— MITRE
Affected Software
Remediation
Event History
Frequently Asked Questions
What is the severity of CVE-2026-29038?
CVE-2026-29038 is classified as a high severity vulnerability due to the potential for reflected cross-site scripting attacks.
How do I fix CVE-2026-29038?
To fix CVE-2026-29038, you should upgrade to version 0.54.4 or later of the changedetection.io package.
What type of vulnerability is CVE-2026-29038?
CVE-2026-29038 is a reflected cross-site scripting (XSS) vulnerability.
Which endpoint is affected by CVE-2026-29038?
The /rss/tag/ endpoint is the specific endpoint affected by CVE-2026-29038.
What impact does CVE-2026-29038 have on users?
CVE-2026-29038 can allow attackers to execute scripts in the context of a user's browser, potentially leading to data theft or session hijacking.