CVE-2023-33977: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Kiwi TCMS is an open source test management system for both manual and automated testing. Kiwi TCMS allows users to upload attachments to test plans, test cases, etc. Earlier versions of Kiwi TCMS had introduced upload validators in order to prevent potentially dangerous files from being uploaded and Content-Security-Policy definition to prevent cross-site-scripting attacks. The upload validation checks were not 100% robust which left the possibility to circumvent them and upload a potentially dangerous file which allows execution of arbitrary JavaScript in the browser. Additionally we've discovered that Nginx's `proxy_pass` directive will strip some headers negating protections built into Kiwi TCMS when served behind a reverse proxy. This issue has been addressed in version 12.4. Users are advised to upgrade. Users unable to upgrade who are serving Kiwi TCMS behind a reverse proxy should make sure that additional header values are still passed to the client browser. If they aren't redefining them inside the proxy configuration.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 5.4
Published: 2023-06-06T19:15Z
Repository: https://github.com/kiwitcms/Kiwi
Language: Python
Files changed: tcms/kiwi_attachments/tests/test_validators.py, tcms/kiwi_attachments/validators.py, tests/ui/data/svg_with_onload_attribute.svg
Commit date: 2023-06-06 00:01:55 +0200
Commit message: Improve upload validation to check for dangerous attributes

Diff: diff --git a/tcms/kiwi_attachments/tests/test_validators.py b/tcms/kiwi_attachments/tests/test_validators.py
index 40b2ae0b1a..ec9228e8d4 100644
--- a/tcms/kiwi_attachments/tests/test_validators.py
+++ b/tcms/kiwi_attachments/tests/test_validators.py
@@ -23,10 +23,23 @@ class TestValidators(APITestCase):
 
             message = str(_("File contains forbidden <script> tag"))
             with self.assertRaisesRegex(Fault, message):
                 self.rpc_client.User.add_attachment("inline_javascript.svg", b64)
 
+    @parameterized.expand(
+        [
+            "svg_with_onload_attribute.svg",
+        ]
+    )
+    def test_uploading_svg_with_forbidden_attributes_should_fail(self, file_name):
+        with open(f"tests/ui/data/{file_name}", "rb") as svg_file:
+            b64 = base64.b64encode(svg_file.read()).decode()
+
+            message = str(_("File contains forbidden attribute:"))
+            with self.assertRaisesRegex(Fault, message):
+                self.rpc_client.User.add_attachment("image.svg", b64)
+
     def test_uploading_filename_ending_in_dot_exe_should_fail(self):
         message = str(_("Uploading executable files is forbidden"))
         with self.assertRaisesRegex(Fault, message):
             self.rpc_client.User.add_attachment("hello.exe", "a2l3aXRjbXM=")
 
diff --git a/tcms/kiwi_attachments/validators.py b/tcms/kiwi_attachments/validators.py
index 696a14399b..26b735b7f7 100644
--- a/tcms/kiwi_attachments/validators.py
+++ b/tcms/kiwi_attachments/validators.py
@@ -5,10 +5,13 @@ from django.utils.translation import gettext_lazy as _
 def deny_uploads_containing_script_tag(uploaded_file):
     for chunk in uploaded_file.chunks(2048):
         if chunk.lower().find(b"<script") > -1:
             raise ValidationError(_("File contains forbidden <script> tag"))
 
+        if chunk.lower().find(b"onload=") > -1:
+            raise ValidationError(_("File contains forbidden attribute:") + "onload")
+
 
 def deny_uploads_ending_in_dot_exe(uploaded_file):
     message = _("Uploading executable files is forbidden")
 
     if uploaded_file.name.find(".exe") > -1:
diff --git a/tests/ui/data/svg_with_onload_attribute.svg b/tests/ui/data/svg_with_onload_attribute.svg
new file mode 100644
index 0000000000..2e8fdf228f
--- /dev/null
+++ b/tests/ui/data/svg_with_onload_attribute.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)"><defs><font id="x"><font-face font-family="y"/></font></defs></svg>

Fixed code:     @parameterized.expand(
        [
            "svg_with_onload_attribute.svg",
        ]
    )
    def test_uploading_svg_with_forbidden_attributes_should_fail(self, file_name):
        with open(f"tests/ui/data/{file_name}", "rb") as svg_file:
            b64 = base64.b64encode(svg_file.read()).decode()

            message = str(_("File contains forbidden attribute:"))
            with self.assertRaisesRegex(Fault, message):
                self.rpc_client.User.add_attachment("image.svg", b64)

        if chunk.lower().find(b"onload=") > -1:
            raise ValidationError(_("File contains forbidden attribute:") + "onload")

<svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)"><defs><font id="x"><font-face font-family="y"/></font></defs></svg>