CVE-2026-43891: changedetection.io: Arbitrary Local File Read via crafted backup restore

Published May 5, 2026
·
Updated

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, dstdir). This preserves attacker-controlled files inside the restored watch directory, including history.txt.

Relevant code: - changedetectionio/blueprint/backups/restore.py - changedetectionio/blueprint/backups/restore.py

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

Relevant code: - changedetectionio/model/Watch.py - changedetectionio/model/Watch.py - changedetectionio/model/Watch.py

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.datadir, v) else: snapshotfname = os.path.basename(v) proposednewpath = os.path.join(self.datadir, snapshotfname) if not os.path.exists(v) and os.path.exists(proposednewpath): v = proposednewpath

This has the following security consequence:

- If the history.txt value is only a filename, it is resolved safely under self.datadir. - 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 gethistorysnapshot(). 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: - changedetectionio/model/Watch.py

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: - changedetectionio/blueprint/ui/preview.py - changedetectionio/api/Watch.py

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

python content = watch.gethistorysnapshot(timestamp=timestamp)

In the API flow, the application also calls:

python content = watch.gethistorysnapshot(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.datadir.

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

2. 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" />

3. 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" />

4. 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

5. Open that watch directory and edit history.txt.

6. 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" />

7. 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" />

8. 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" />

9. After restore completes, open Preview for the restored watch.

10. 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.gethistorysnapshot(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.datadir. 4. Skip the entry if the resolved file does not exist inside the watch directory.

Suggested code change:

python snapshotfname = os.path.basename(v.strip()) resolvedpath = os.path.join(self.datadir, snapshotfname)

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

tmphistory[k] = resolvedpath

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.

Other sources

changedetection.io is a free open source web page change detection tool. Prior to 0.55.1, 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, dstdir). This preserves attacker-controlled files inside the restored watch directory, including history.txt. After restore, the application parses history.txt in the watch history property and returns the contents of the targeted local file. This vulnerability is fixed in 0.55.1.

MITRE

Affected Software

2 affected componentsFixes available
pip/changedetection.io<=0.54.10
0.55.1
Webtechnologies Changedetection<0.55.1

Event History

May 5, 2026
Advisory Published
via GitHub·09:16 PM
Data Sourced
via GitHub·09:16 PM
DescriptionSeverityWeaknessAffected Software
May 12, 2026
CVE Published
via MITRE·04:56 PM
Data Sourced
via MITRE·04:56 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·06:17 PM
DescriptionSeverityWeaknessAffected Software
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-43891?

CVE-2026-43891 is considered a high severity vulnerability due to its potential exploitation through untrusted backup restore operations.

2

How do I fix CVE-2026-43891?

To fix CVE-2026-43891, update to the latest version of changedetection.io, specifically version 0.55.1 or later.

3

What causes the vulnerability CVE-2026-43891?

CVE-2026-43891 is caused by trusting attacker-controlled snapshot paths that are restored from backup ZIP files.

4

Which versions of changedetection.io are affected by CVE-2026-43891?

Versions of changedetection.io up to and including 0.54.10 are affected by CVE-2026-43891.

5

What are the potential impacts of CVE-2026-43891?

The potential impacts of CVE-2026-43891 include unauthorized file access leading to data breaches or system compromise.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203