Skip to main content

changedetection.io CVE-2026-43891

HIGH
External Control of File Name or Path (CWE-73)
2026-05-05 https://github.com/dgtlmoon/changedetection.io GHSA-8757-69j2-hx56
7.5
CVSS 3.1 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
7.5 HIGH
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Primary rating from GitHub Advisory · only source for this CVE.

CVSS VectorGitHub Advisory

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

Lifecycle Timeline

2
Source Code Evidence Fetched
May 05, 2026 - 21:46 vuln.today
Analysis Generated
May 05, 2026 - 21:46 vuln.today

DescriptionGitHub Advisory

Details

The vulnerability is caused by trusting attacker-controlled snapshot paths restored from backup files.

The vulnerable flow starts in the backup restore logic. When a backup ZIP is restored, the application extracts the archive and copies each restored watch UUID directory directly into the live datastore using shutil.copytree(entry.path, dst_dir). This preserves attacker-controlled files inside the restored watch directory, including history.txt.

Relevant code:

After restore, the application parses history.txt in the watch history property. This is the core trust-boundary issue.

Relevant code:

The relevant logic is effectively:

python
if os.sep not in v and '/' not in v and '\\' not in v:
    v = os.path.join(self.data_dir, v)
else:
    snapshot_fname = os.path.basename(v)
    proposed_new_path = os.path.join(self.data_dir, snapshot_fname)
    if not os.path.exists(v) and os.path.exists(proposed_new_path):
        v = proposed_new_path

This has the following security consequence:

  • If the history.txt value is only a filename, it is resolved safely under self.data_dir.
  • If the value contains path separators, it is treated as a path reference rather than a watch-local snapshot name.
  • If that external path already exists, it is preserved unchanged.

As a result, a malicious restored history.txt entry such as:

1776969105,/etc/passwd

will be accepted if the referenced file exists and is readable by the application process.

The second vulnerable step is in get_history_snapshot(). Once the untrusted path has been accepted into the watch history, the application reads the resolved path directly without enforcing that it remains inside the watch directory.

Relevant code:

That function eventually performs direct file reads such as:

python
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
    return f.read()

The third step is reachability. The trusted history entry is consumed by both the Preview UI and the watch history API.

Relevant code:

In the Preview flow, the application selects the latest history timestamp and calls:

python
content = watch.get_history_snapshot(timestamp=timestamp)

In the API flow, the application also calls:

python
content = watch.get_history_snapshot(timestamp=timestamp)

This creates the following end-to-end exploit chain:

  1. An attacker supplies a crafted backup ZIP.
  2. The restore process preserves attacker-controlled history.txt.
  3. The history.txt parser accepts an absolute or out-of-directory path if that path exists.
  4. Preview or the history API dereferences the stored path directly.
  5. The application returns the contents of the targeted local file.

The root cause is that imported history entries are treated as trusted filesystem paths instead of being restricted to safe basenames under watch.data_dir.

PoC

The following proof of concept demonstrates the end-to-end exploit chain. It assumes the attacker has gained access to the backup restore functionality to upload the crafted archive.

  1. Create a normal watch in the UI, for example:
text
https://example.com
  1. Trigger at least one successful check so the watch generates a valid history entry and can later be included in a backup.

<img width="1230" height="587" alt="image" src="https://github.com/user-attachments/assets/a76302c9-48d6-4aa3-9bfc-0ba2fdb31156" />

  1. Go to the Backups section and create a backup archive.

<img width="1232" height="390" alt="image" src="https://github.com/user-attachments/assets/8fef0444-d8f0-4db6-be75-04fa7db2fec8" />

  1. Extract the backup archive and identify the watch UUID directory that contains the target watch's watch.json. For example:
text
5db3d3d8-71e6-4db2-a81e-e1f0445c3e47
  1. Open that watch directory and edit history.txt.
  2. Replace the latest history entry with a path to an existing local file that is readable by the application process. For example:
text
1776969188,/etc/passwd

If the timestamp differs in the extracted backup, keep the original latest timestamp and only replace the filename/path portion.

Example:

text
1776969188,742215043ff9be7e635f05e680ff9b11.txt

becomes:

text
1776969188,/etc/passwd

<img width="1139" height="366" alt="image" src="https://github.com/user-attachments/assets/07277b36-4747-4181-82d1-523385b40de3" />

  1. Repack the backup so that the UUID directories are located at the root of the ZIP archive.

Important:

  • Do not add an extra parent directory layer when repacking.
  • The archive root should contain directories such as:
text
<watch-uuid>/
<group-uuid>/
changedetection.json
url-list.txt

<img width="986" height="353" alt="image" src="https://github.com/user-attachments/assets/21b30368-2cb5-4b88-9055-79d5bf06ad48" />

  1. In the UI, restore the modified backup and enable replacement of existing watches with the same UUID.

<img width="1229" height="700" alt="image" src="https://github.com/user-attachments/assets/79abd9d2-8b2d-4e08-abf3-3b63238a8055" />

  1. After restore completes, open Preview for the restored watch.
  2. The application will read the attacker-controlled path from history.txt and display the contents of the referenced local file instead of the original watch snapshot.

Observed result:

  • The Preview page returns the content of the attacker-selected local file.

Expected result:

  • The application should reject absolute paths or out-of-directory paths restored from history.txt.
  • Snapshot history should be restricted to files within the watch's own data directory.

Optional API verification:

  • The same issue can also be confirmed through the watch history API by requesting the modified timestamp after restore.
  • The API returns the same file content because it also calls watch.get_history_snapshot(timestamp=timestamp) on the trusted history entry.

<img width="1233" height="545" alt="image" src="https://github.com/user-attachments/assets/a2814a5a-2cdd-49a1-916b-c956dd5fda1f" />

Impact

This is an arbitrary local file disclosure vulnerability reachable through malicious backup restore content.

Who is impacted:

  • Deployments where the application process has read access to sensitive local system files.
  • Docker or host-mounted environments where secrets, config files, or operational artifacts are explicitly readable by the service.

What can be exposed:

  • Arbitrary System Files: Core operating system files (e.g., /etc/passwd, /proc/self/environ), system-level configurations, and host metrics.
  • Application Data: Internal records and files residing under the /datastore directory.
  • Secrets & Artifacts: Application-local configuration files, API tokens, database credentials, and other sensitive artifacts accessible to the application process.

By accessing the backup restore functionality and importing a crafted archive, an attacker can exploit the application's fail-open path validation. The confidentiality impact is exceptionally high because, once the payload is ingested, the application can be manipulated to disclose arbitrary local system files and highly sensitive environment variables directly through standard UI or API responses.

Recommendation

The application should treat all paths restored from history.txt as untrusted input.

The root cause is in changedetectionio/model/Watch.py, where values containing path separators are currently accepted as filesystem paths and preserved if the referenced file already exists.

The fix should be:

  1. Never trust absolute or external paths from history.txt.
  2. Normalize every history entry to os.path.basename(v).
  3. Join the normalized filename to self.data_dir.
  4. Skip the entry if the resolved file does not exist inside the watch directory.

Suggested code change:

python
snapshot_fname = os.path.basename(v.strip())
resolved_path = os.path.join(self.data_dir, snapshot_fname)

if not os.path.exists(resolved_path):
    logger.warning(
        f"Skipping unsafe or missing history entry for {self.get('uuid')}: {v!r}"
    )
    continue

tmp_history[k] = resolved_path

This ensures restored history entries can only reference files inside the watch's own data directory and prevents arbitrary local file reads through Preview or the history API.

AnalysisAI

Arbitrary local file disclosure in changedetection.io allows remote unauthenticated attackers to read sensitive system files via crafted backup archives. When a malicious backup ZIP is uploaded and restored, the application trusts attacker-controlled paths in the history.txt file, enabling reads of files like /etc/passwd, environment variables, application secrets, and mounted Docker volumes through the Preview UI or history API. This vulnerability (CVSS 7.5) affects all versions through 0.54.10, with fix available in 0.55.1. No active exploitation (KEV) confirmed, but a detailed proof-of-concept exists demonstrating the complete attack chain from backup modification to file exfiltration. EPSS data not available, but the combination of network attack vector, no authentication requirement, and public exploit code makes this a priority for immediate patching.

Technical ContextAI

The vulnerability stems from CWE-73 (External Control of File Name or Path) in the Python-based changedetection.io application's backup restore mechanism. The affected code in changedetectionio/model/Watch.py fails to sanitize file paths restored from backup archives' history.txt files. When processing watch history entries, the application checks if a value contains path separators (os.sep, '/', '\') and if the referenced external path exists, it preserves the attacker-controlled absolute or relative path unchanged instead of normalizing to a safe basename. This path is then passed directly to Python's open() function in get_history_snapshot() without directory traversal validation. The backup restore flow uses shutil.copytree() to transplant entire watch UUID directories from untrusted ZIP archives into the live datastore, preserving malicious history.txt payloads. The vulnerable code path is reachable through both the web UI Preview feature (changedetectionio/blueprint/ui/preview.py) and the REST API watch history endpoint (changedetectionio/api/Watch.py), both of which call get_history_snapshot() on trusted history entries. The CPE identifier pkg:pip/changedetection.io indicates this is a Python package distributed via PyPI, commonly deployed in Docker containers with host filesystem mounts.

RemediationAI

Upgrade to changedetection.io version 0.55.1 or later immediately. For PyPI installations, run 'pip install --upgrade changedetection.io>=0.55.1'. For Docker deployments, update container images to 'dgtlmoon/changedetection.io:0.55.1' or later and redeploy. The fix normalizes all history.txt paths to safe basenames using os.path.basename() and restricts resolution to the watch's data directory, preventing directory traversal. Until patching is complete, implement these compensating controls: (1) Restrict access to the /backups/restore endpoint to only trusted administrators via reverse proxy ACLs or application authentication (verify current auth status first-if already authenticated-only, this reduces risk). Trade-off: may complicate legitimate backup restore workflows. (2) Run the application process with minimal filesystem permissions using Docker user remapping (--userns-remap) or Kubernetes securityContext, limiting readable files to application data only. Trade-off: requires container reconfiguration and testing. (3) Enable read-only root filesystem in Docker (--read-only flag with tmpfs mounts for /tmp) to prevent write access to restored malicious history.txt, though this does not prevent reads of existing files. Trade-off: requires application testing for write path compatibility. (4) Monitor file access logs for unexpected reads outside /datastore directory, especially /etc/, /proc/, and mounted secret paths. These mitigations reduce but do not eliminate risk-patching to 0.55.1 is the only complete remediation. See vendor advisory at https://github.com/dgtlmoon/changedetection.io/security/advisories/GHSA-8757-69j2-hx56.

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2026-55255 HIGH POC
8.4 Jun 19

Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing

Share

CVE-2026-43891 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy