CVE-2024-34061: Insufficient Information (NVD-CWE-noinfo)
value: 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.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-02T14:15Z
Repository: https://github.com/dgtlmoon/changedetection.io
Language: HTML
Files changed: changedetectionio/blueprint/tags/templates/edit-tag.html, changedetectionio/blueprint/tags/templates/groups-overview.html, changedetectionio/templates/IMPORTANT.md, changedetectionio/templates/_common_fields.html, changedetectionio/templates/diff.html, changedetectionio/templates/edit.html, changedetectionio/templates/import.html, changedetectionio/templates/settings.html, changedetectionio/templates/watch-overview.html, changedetectionio/tests/test_security.py
Commit date: 2024-05-02 11:46:31 +0200
Commit message: 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.
Diff: diff --git a/changedetectionio/blueprint/tags/templates/edit-tag.html b/changedetectionio/blueprint/tags/templates/edit-tag.html
index 7c3bf42b..1d297c81 100644
--- a/changedetectionio/blueprint/tags/templates/edit-tag.html
+++ b/changedetectionio/blueprint/tags/templates/edit-tag.html
@@ -1,9 +1,9 @@
 {% 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>
 
 <script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
diff --git a/changedetectionio/blueprint/tags/templates/groups-overview.html b/changedetectionio/blueprint/tags/templates/groups-overview.html
index 23eca0a6..30ddfe18 100644
--- a/changedetectionio/blueprint/tags/templates/groups-overview.html
+++ b/changedetectionio/blueprint/tags/templates/groups-overview.html
@@ -1,8 +1,8 @@
 {% 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">
     <form class="pure-form" action="{{ url_for('tags.form_tag_add') }}" method="POST" id="new-watch-form">
         <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" >
diff --git a/changedetectionio/templates/IMPORTANT.md b/changedetectionio/templates/IMPORTANT.md
new file mode 100644
index 00000000..4b769264
--- /dev/null
+++ b/changedetectionio/templates/IMPORTANT.md
@@ -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
+
diff --git a/changedetectionio/templates/_common_fields.jinja b/changedetectionio/templates/_common_fields.html
similarity index 99%
rename from changedetectionio/templates/_common_fields.jinja
rename to changedetectionio/templates/_common_fields.html
index 8b44ebbd..932f3fb7 100644
--- a/changedetectionio/templates/_common_fields.jinja
+++ b/changedetectionio/templates/_common_fields.html
@@ -1,7 +1,7 @@
 
-{% 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">
                             {{ render_field(form.notification_urls, rows=5, placeholder="Examples:
     Gitter - gitter://token/room
diff --git a/changedetectionio/templates/_helpers.jinja b/changedetectionio/templates/_helpers.html
similarity index 100%
rename from changedetectionio/templates/_helpers.jinja
rename to changedetectionio/templates/_helpers.html
diff --git a/changedetectionio/templates/diff.html b/changedetectionio/templates/diff.html
index 0197bfc9..c82a559e 100644
--- a/changedetectionio/templates/diff.html
+++ b/changedetectionio/templates/diff.html
@@ -1,7 +1,7 @@
 {% 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)}}";
     {% if last_error_screenshot %}
     const error_screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid, error_screenshot=1) }}";
diff --git a/changedetectionio/templates/edit.html b/changedetectionio/templates/edit.html
index c5493192..d10df405 100644
--- a/changedetectionio/templates/edit.html
+++ b/changedetectionio/templates/edit.html
@@ -1,9 +1,9 @@
 {% 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>
     const browser_steps_available_screenshots=JSON.parse('{{ watch.get_browsersteps_available_screenshots|tojson }}');
     const browser_steps_config=JSON.parse('{{ browser_steps_config|tojson }}');
diff --git a/changedetectionio/templates/import.html b/changedetectionio/templates/import.html
index 1896080c..52d878de 100644
--- a/changedetectionio/templates/import.html
+++ b/changedetectionio/templates/import.html
@@ -1,8 +1,8 @@
 {% 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">
 
     <div class="tabs collapsable">
         <ul>
diff --git a/changedetectionio/templates/settings.html b/changedetectionio/templates/settings.html
index 16030a2f..78387a48 100644
--- a/changedetectionio/templates/settings.html
+++ b/changedetectionio/templates/settings.html
@@ -1,10 +1,10 @@
 {% 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 %}
     const email_notification_prefix=JSON.parse('{{emailprefix|tojson}}');
 {% endif %}
diff --git a/changedetectionio/templates/watch-overview.html b/changedetectionio/templates/watch-overview.html
index 03cc21ca..6a25208e 100644
--- a/changedetectionio/templates/watch-overview.html
+++ b/changedetectionio/templates/watch-overview.html
@@ -1,8 +1,8 @@
 {% 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>
 
 <div class="box">
 
diff --git a/changedetectionio/tests/test_security.py b/changedetectionio/tests/test_security.py
index 406a5401..c56c8449 100644
--- a/changedetectionio/tests/test_security.py
+++ b/changedetectionio/tests/test_security.py
@@ -1,12 +1,14 @@
 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'},
         follow_redirects=True
     )
@@ -61,6 +63,27 @@ def test_bad_access(client, live_server):
         follow_redirects=True
     )
     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
+

Vulnerable code: {% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
{% from '_common_fields.jinja' import render_common_settings_form %}
{% from '_helpers.jinja' import render_simple_field, render_field %}
{% from '_helpers.jinja' import render_field %}
{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
{% from '_common_fields.jinja' import render_common_settings_form %}
{% from '_helpers.jinja' import render_field %}
{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
{% from '_common_fields.jinja' import render_common_settings_form %}
{% from '_helpers.jinja' import render_simple_field, render_field, render_nolabel_field, sort_by_title %}
    live_server_setup(live_server)
    assert b'file:// type access is denied for security reasons.' in res.data
Fixed code: {% from '_helpers.html' import render_field, render_checkbox_field, render_button %}
{% from '_common_fields.html' import render_common_settings_form %}
{% from '_helpers.html' import render_simple_field, render_field %}
# 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

{% from '_helpers.html' import render_field %}
{% from '_helpers.html' import render_field, render_checkbox_field, render_button %}
{% from '_helpers.html' import render_field, render_checkbox_field, render_button %}
{% from '_common_fields.html' import render_common_settings_form %}
{% from '_helpers.html' import render_field %}
{% from '_helpers.html' import render_field, render_checkbox_field, render_button %}
{% from '_common_fields.html' import render_common_settings_form %}
{% from '_helpers.html' import render_simple_field, render_field, render_nolabel_field, sort_by_title %}
def test_setup(client, live_server):
    live_server_setup(live_server)
    #live_server_setup(live_server)
    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