{"id":"GHSA-8whx-v8qq-pq64","summary":"changedetection.io has Reflected XSS in its RSS Tag Error Response","details":"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.\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:** \u003c= 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 `\u003clink\u003e` HTML tag on the homepage without authentication:\n\n1.  Attacker visits the target's homepage (if unauthenticated) and extracts the RSS token from the `\u003clink\u003e` tag\n2.  Crafts a malicious URL:\n    \n    ```\n    http://target:5000/rss/tag/\u003cimg src=x onerror=alert(document.cookie)\u003e?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 \u003cimg src=x onerror=alert(document.cookie)\u003e not found\n    \n    ```\n    \n5.  The browser renders the `\u003cimg\u003e` 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 \u003cimg src=x onerror=alert(document.domain)\u003e 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 `\u003clink\u003e` 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)","aliases":["CVE-2026-29038","PYSEC-2026-2127"],"modified":"2026-07-13T07:26:50.318400804Z","published":"2026-03-04T20:58:14Z","database_specific":{"github_reviewed_at":"2026-03-04T20:58:14Z","nvd_published_at":"2026-03-06T07:16:01Z","cwe_ids":["CWE-79"],"severity":"MODERATE","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/dgtlmoon/changedetection.io/security/advisories/GHSA-8whx-v8qq-pq64"},{"type":"WEB","url":"https://github.com/dgtlmoon/changedetection.io/security/advisories/GHSA-mw8m-398g-h89w"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-29038"},{"type":"WEB","url":"https://github.com/dgtlmoon/changedetection.io/commit/ec7d56f85d1e9690fca7cb4711c1fb20dffec780"},{"type":"PACKAGE","url":"https://github.com/dgtlmoon/changedetection.io"},{"type":"WEB","url":"https://github.com/dgtlmoon/changedetection.io/releases/tag/0.54.4"}],"affected":[{"package":{"name":"changedetection-io","ecosystem":"PyPI","purl":"pkg:pypi/changedetection-io"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.54.4"}]}],"versions":["0.38.2","0.39","0.39.1","0.39.10","0.39.10.post1","0.39.10.post2","0.39.11","0.39.12","0.39.13","0.39.13.1","0.39.14","0.39.14.1","0.39.15","0.39.16","0.39.17","0.39.17.1","0.39.17.2","0.39.18","0.39.19","0.39.19.1","0.39.2","0.39.20","0.39.20.1","0.39.20.2","0.39.20.3","0.39.20.4","0.39.21","0.39.21.1","0.39.22","0.39.22.1","0.39.3","0.39.4","0.39.5","0.39.6","0.39.7","0.39.8","0.39.9","0.40.0","0.40.0.1","0.40.0.2","0.40.0.3","0.40.0.4","0.40.1.0","0.40.1.1","0.40.2","0.40.3","0.41","0.41.1","0.42","0.42.1","0.42.2","0.42.3","0.43.1","0.43.2","0.44","0.44.1","0.45","0.45.1","0.45.11","0.45.12","0.45.13","0.45.14","0.45.15","0.45.16","0.45.17","0.45.18","0.45.19","0.45.2","0.45.20","0.45.21","0.45.22","0.45.23","0.45.24","0.45.25","0.45.26","0.45.3","0.45.4","0.45.5","0.45.6","0.45.7","0.45.7.1","0.45.7.2","0.45.7.3","0.45.8","0.45.8.1","0.45.9","0.46.0","0.46.1","0.46.2","0.46.3","0.46.4","0.47.0","0.47.1","0.47.2","0.47.3","0.47.4","0.47.5","0.47.6","0.48.0","0.48.1","0.48.2","0.48.3","0.48.4","0.48.5","0.48.6","0.49.0","0.49.1","0.49.10","0.49.12","0.49.13","0.49.14","0.49.15","0.49.16","0.49.17","0.49.18","0.49.2","0.49.3","0.49.4","0.49.5","0.49.6","0.49.7","0.49.8","0.49.9","0.50.1","0.50.10","0.50.11","0.50.12","0.50.13","0.50.14","0.50.15","0.50.16","0.50.17","0.50.18","0.50.19","0.50.2","0.50.20","0.50.21","0.50.22","0.50.23","0.50.24","0.50.25","0.50.26","0.50.27","0.50.28","0.50.29","0.50.3","0.50.30","0.50.31","0.50.32","0.50.33","0.50.34","0.50.35","0.50.37","0.50.38","0.50.39","0.50.4","0.50.40","0.50.41","0.50.42","0.50.43","0.50.5","0.50.6","0.50.7","0.50.8","0.50.9","0.51.0","0.51.1","0.51.2","0.51.3","0.51.4","0.52.1","0.52.2","0.52.3","0.52.4","0.52.5","0.52.6","0.52.7","0.52.8","0.52.9","0.53.1","0.53.2","0.53.3","0.53.4","0.53.5","0.53.6","0.53.7","0.54.1","0.54.2","0.54.3"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-8whx-v8qq-pq64/GHSA-8whx-v8qq-pq64.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"}]}