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].
- GitHub - dgtlmoon/changedetection.io: Best and simplest tool for website change detection, web page monitoring, and website change alerts. Perfect for tracking content changes, price drops, restock alerts, and website defacement monitoring—all for free or enjoy our SaaS plan!
- Merge pull request from GHSA-pwgc-w4x9-gw67 · dgtlmoon/changedetection.io@c0f000b
- NVD - CVE-2024-34061
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.
| Package | Affected versions | Patched versions |
|---|---|---|
changedetection.ioPyPI | < 0.45.22 | 0.45.22 |
Affected products
2- Range: 0.1, 0.11, 0.12, …
Patches
1c0f000b1d1ceMerge 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 renamedchangedetectionio/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"<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- github.com/advisories/GHSA-pwgc-w4x9-gw67ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-34061ghsaADVISORY
- github.com/dgtlmoon/changedetection.io/blob/0.45.21/changedetectionio/forms.pyghsaWEB
- github.com/dgtlmoon/changedetection.io/commit/c0f000b1d1ce03733460805dbbedde445fe2c762nvdWEB
- github.com/dgtlmoon/changedetection.io/security/advisories/GHSA-pwgc-w4x9-gw67nvdWEB
News mentions
0No linked articles in our index yet.