VYPR
Medium severity4.3OSV Advisory· Published May 2, 2024· Updated Apr 15, 2026

CVE-2024-34061

CVE-2024-34061

Description

changedetection.io is a free open source web page change detection, website watcher, restock monitor and notification service. In affected versions Input in parameter notification_urls is not processed resulting in javascript execution in the application. A reflected XSS vulnerability happens when the user input from a URL or POST data is reflected on the page without being stored, thus allowing the attacker to inject malicious content. This issue has been addressed in version 0.45.22. Users are advised to upgrade. There are no known workarounds for this vulnerability.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A reflected XSS vulnerability in changedetection.io before 0.45.22 allows an attacker to inject JavaScript via the notification_urls parameter.

Vulnerability

Overview

The changedetection.io application, which monitors web page changes and sends notifications, has a reflected cross-site scripting (XSS) vulnerability in the notification_urls parameter. The official description confirms that user input in this parameter is not properly sanitized, allowing the execution of JavaScript in the application's context [1]. The root cause is related to how templates handle imports and autoescaping; the fix involves renaming template files from .jinja to .html to ensure Jinja2's autoescaping is applied (as shown in the commit diff where template imports are changed from _helpers.jinja to _helpers.html) [2].

Exploitation and

Attack Vector

This is a reflected XSS vulnerability, meaning the malicious payload is not stored but is immediately reflected back in the server's response. The attack can be triggered by crafting a URL or POST request containing JavaScript code in the notification_urls parameter and then luring a victim to interact with that URL [3]. No authentication is required for the initial injection, though the victim must be using the application's web interface for the script to execute in their session.

Impact and

Consequences

An attacker can inject arbitrary JavaScript into the page, potentially leading to session hijacking, data theft, or phishing attacks by manipulating the page content. Since the vulnerability is reflected, it relies on social engineering to deliver the malicious link or form submission, but the consequences in a browser session are similar to stored XSS [3].

Mitigation and

Remediation

The vulnerability has been addressed in version 0.45.22 of changedetection.io [1]. Users are strongly advised to upgrade to this or a later version. According to the advisory, there are no known workarounds for this issue [1]. The fix ensures that template files are parsed with the correct MIME type so that Jinja2's autoescaping is enabled, preventing the injection of raw HTML or JavaScript [2].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
changedetection.ioPyPI
< 0.45.220.45.22

Affected products

2

Patches

1
c0f000b1d1ce

Merge pull request from GHSA-pwgc-w4x9-gw67

11 files changed · +42 13
  • changedetectionio/blueprint/tags/templates/edit-tag.html+2 2 modified
    @@ -1,7 +1,7 @@
     {% extends 'base.html' %}
     {% block content %}
    -{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
    -{% from '_common_fields.jinja' import render_common_settings_form %}
    +{% from '_helpers.html' import render_field, render_checkbox_field, render_button %}
    +{% from '_common_fields.html' import render_common_settings_form %}
     <script>
         const notification_base_url="{{url_for('ajax_callback_send_notification_test', mode="group-settings")}}";
     </script>
    
  • changedetectionio/blueprint/tags/templates/groups-overview.html+1 1 modified
    @@ -1,6 +1,6 @@
     {% extends 'base.html' %}
     {% block content %}
    -{% from '_helpers.jinja' import render_simple_field, render_field %}
    +{% from '_helpers.html' import render_simple_field, render_field %}
     <script src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script>
     
     <div class="box">
    
  • changedetectionio/templates/_common_fields.html+1 1 renamed
    @@ -1,5 +1,5 @@
     
    -{% from '_helpers.jinja' import render_field %}
    +{% from '_helpers.html' import render_field %}
     
     {% macro render_common_settings_form(form, emailprefix, settings_application) %}
                             <div class="pure-control-group">
    
  • changedetectionio/templates/diff.html+1 1 modified
    @@ -1,5 +1,5 @@
     {% extends 'base.html' %}
    -{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
    +{% from '_helpers.html' import render_field, render_checkbox_field, render_button %}
     {% block content %}
     <script>
         const screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid)}}";
    
  • changedetectionio/templates/edit.html+2 2 modified
    @@ -1,7 +1,7 @@
     {% extends 'base.html' %}
     {% block content %}
    -{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
    -{% from '_common_fields.jinja' import render_common_settings_form %}
    +{% from '_helpers.html' import render_field, render_checkbox_field, render_button %}
    +{% from '_common_fields.html' import render_common_settings_form %}
     <script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
     <script src="{{url_for('static_content', group='js', filename='vis.js')}}" defer></script>
     <script>
    
  • changedetectionio/templates/_helpers.html+0 0 renamed
  • changedetectionio/templates/IMPORTANT.md+6 0 added
    @@ -0,0 +1,6 @@
    +# Important notes about templates
    +
    +Template names should always end in ".html", ".htm", ".xml", ".xhtml", ".svg", even the `import`'ed templates.
    +
    +Jinja2's `def select_jinja_autoescape(self, filename: str) -> bool:` will check the filename extension and enable autoescaping
    +
    
  • changedetectionio/templates/import.html+1 1 modified
    @@ -1,6 +1,6 @@
     {% extends 'base.html' %}
     {% block content %}
    -{% from '_helpers.jinja' import render_field %}
    +{% from '_helpers.html' import render_field %}
     <script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
     <div class="edit-form monospaced-textarea">
     
    
  • changedetectionio/templates/settings.html+2 2 modified
    @@ -1,8 +1,8 @@
     {% extends 'base.html' %}
     
     {% block content %}
    -{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
    -{% from '_common_fields.jinja' import render_common_settings_form %}
    +{% from '_helpers.html' import render_field, render_checkbox_field, render_button %}
    +{% from '_common_fields.html' import render_common_settings_form %}
     <script>
         const notification_base_url="{{url_for('ajax_callback_send_notification_test', mode="global-settings")}}";
     {% if emailprefix %}
    
  • changedetectionio/templates/watch-overview.html+1 1 modified
    @@ -1,6 +1,6 @@
     {% extends 'base.html' %}
     {% block content %}
    -{% from '_helpers.jinja' import render_simple_field, render_field, render_nolabel_field, sort_by_title %}
    +{% from '_helpers.html' import render_simple_field, render_field, render_nolabel_field, sort_by_title %}
     <script src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script>
     <script src="{{url_for('static_content', group='js', filename='watch-overview.js')}}" defer></script>
     
    
  • changedetectionio/tests/test_security.py+25 2 modified
    @@ -2,9 +2,11 @@
     from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks
     import time
     
    +def test_setup(client, live_server):
    +    live_server_setup(live_server)
     
     def test_bad_access(client, live_server):
    -    live_server_setup(live_server)
    +    #live_server_setup(live_server)
         res = client.post(
             url_for("import_page"),
             data={"urls": 'https://localhost'},
    @@ -63,4 +65,25 @@ def test_bad_access(client, live_server):
         wait_for_all_checks(client)
         res = client.get(url_for("index"))
     
    -    assert b'file:// type access is denied for security reasons.' in res.data
    \ No newline at end of file
    +    assert b'file:// type access is denied for security reasons.' in res.data
    +
    +def test_xss(client, live_server):
    +    #live_server_setup(live_server)
    +    from changedetectionio.notification import (
    +        default_notification_format
    +    )
    +    # the template helpers were named .jinja which meant they were not having jinja2 autoescape enabled.
    +    res = client.post(
    +        url_for("settings_page"),
    +        data={"application-notification_urls": '"><img src=x onerror=alert(document.domain)>',
    +              "application-notification_title": '"><img src=x onerror=alert(document.domain)>',
    +              "application-notification_body": '"><img src=x onerror=alert(document.domain)>',
    +              "application-notification_format": default_notification_format,
    +              "requests-time_between_check-minutes": 180,
    +              'application-fetch_backend': "html_requests"},
    +        follow_redirects=True
    +    )
    +
    +    assert b"<img src=x onerror=alert(" not in res.data
    +    assert b"&lt;img" in res.data
    +
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.