CVE-2024-34061 (published 2024-05-02T14:15Z)

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.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/dgtlmoon/changedetection.io
commit_details: repo_url: https://github.com/dgtlmoon/changedetection.io
author: dgtlmoon
author_date: 2024-05-02 11:46:31+02:00
msg: Merge pull request from GHSA-pwgc-w4x9-gw67

* Auto-escape was not enabled GHSA-pwgc-w4x9-gw67

* Auto-escape was not enabled because the filenames were not something jinja2 enables it for.
repository: repo_url: https://github.com/dgtlmoon/changedetection.io
repo_name: dgtlmoon/changedetection.io
description: The best and simplest free open source web page change detection, website watcher,  restock monitor and notification service. Restock Monitor, change detection. Designed for simplicity - Simply monitor which websites had a text change for free. Free Open source web page change detection, Website defacement monitoring, Price change notification
repo_language: Python
owner: dgtlmoon
file_changes: filename: edit-tag.html
change_type: ModificationType.MODIFY
diff: @@ -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>

programming_language: HTML
filename: groups-overview.html
change_type: ModificationType.MODIFY
diff: @@ -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">

programming_language: HTML
filename: IMPORTANT.md
change_type: ModificationType.ADD
diff: @@ -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
+

programming_language: SQL
filename: _common_fields.html
change_type: ModificationType.RENAME
diff: @@ -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">

programming_language: HTML
filename: diff.html
change_type: ModificationType.MODIFY
diff: @@ -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)}}";

programming_language: HTML
filename: edit.html
change_type: ModificationType.MODIFY
diff: @@ -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>

programming_language: HTML
filename: import.html
change_type: ModificationType.MODIFY
diff: @@ -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">
 

programming_language: HTML
filename: settings.html
change_type: ModificationType.MODIFY
diff: @@ -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 %}

programming_language: HTML
filename: watch-overview.html
change_type: ModificationType.MODIFY
diff: @@ -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>
 

programming_language: HTML
filename: test_security.py
change_type: ModificationType.MODIFY
diff: @@ -2,9 +2,11 @@ from flask import url_for
 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
+

programming_language: Python