CVE-2026-29038 is a medium-severity (CVSS 6.1) Cross-site Scripting (XSS) vulnerability in changedetection-io. A fix is available for changedetection-io — see the affected versions and patch details below.
changedetection.io: Reflected XSS in RSS Tag Error Response
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-29038.
EPSS Exploitation Probability
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
CVE-2026-29038 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 378,156 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
changedetection-ioReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
A reflected cross-site scripting (XSS) vulnerability was identified in the /rss/tag/ endpoint of changedetection.io. The tag_uuid 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 tag_uuid parameter from the URL path is interpolated into the response body using an f-string with no escaping:
tag = datastore.data['settings']['application'].get('tags', {}).get(tag_uuid)
if not tag:
return f"Tag with UUID {tag_uuid} 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 {tag_uuid} 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/ (single_watch.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:
-
Attacker visits the target's homepage (if unauthenticated) and extracts the RSS token from the
<link>tag -
Crafts a malicious URL:
http://target:5000/rss/tag/<img src=x onerror=alert(document.cookie)>?token=EXTRACTED_TOKEN -
Sends the link to a victim who has an active session on the changedetection.io instance
-
When the victim clicks the link, the server responds with:
Tag with UUID <img src=x onerror=alert(document.cookie)> not found -
The browser renders the
<img>tag, theonerrorfires, and JavaScript executes in the victim's session context
Proof of Concept
Request
GET /rss/tag/%3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3E?token=60b83b06df98b24c66367bc3d233105b HTTP/1.1
Host: localhost:5000
Response
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.cookieexfiltration - Account takeover if session cookies lack the
HttpOnlyflag - 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 tag_uuid parameter before reflecting it in the response, or set the Content-Type to text/plain:
Option A: HTML Escape (Recommended)
from markupsafe import escape
if not tag:
return f"Tag with UUID {escape(tag_uuid)} not found", 404
Option B: Set Content-Type to text/plain
from flask import make_response
if not tag:
resp = make_response(f"Tag with UUID {tag_uuid} 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
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | changedetection-io | all versions | 0.54.4pip install --upgrade 'changedetection-io==0.54.4' |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for changedetection-io, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update changedetection-io to 0.54.4 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-29038 is resolved across your whole dependency graph.
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.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-29038 can be triaged on real exposure rather than presence alone.
Tailored to CVE-2026-29038. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is CVE-2026-29038 in your dependencies?
O3 Security finds CVE-2026-29038 across PyPI dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.