GitHub Security Advisory: GHSA-8whx-v8qq-pq64
Package: changedetection.io (PIP)
Severity: MODERATE (CVSS 6.1)
CWE: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Vulnerable versions: < 0.54.4
Patched in: 0.54.4

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.

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](https://raw.githubusercontent.com/dgtlmoon/changedetection.io/1d72716c6988a4f6796bb85a5d42872800cd7a70/changedetectionio/blueprint/rss/tag.py)

The `tag_uuid` 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(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:

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=EXTRACTED_TOKEN
    
    ```
    
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

```

###