CVE-2023-27489: 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 accepts SVG files uploaded by users which could potentially contain JavaScript code. If SVG images are viewed directly, i.e. not rendered in an HTML page, this JavaScript code could execute. This vulnerability has been fixed by configuring Kiwi TCMS to serve with the Content-Security-Policy HTTP header which blocks inline JavaScript in all modern browsers. This configuration change is provided in version 12.1 and users are advised to upgrade. Users unable to upgrade may set their Content-Security-Policy HTTP header manually.
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-03-29T19:15Z
Repository: https://github.com/kiwitcms/Kiwi
Language: Python
Files changed: etc/nginx.conf, tcms/core/middleware.py, tcms/settings/devel.py, tests/test_http.sh
Commit date: 2023-03-08 14:25:52 +0200
Commit message: Define Content-Security-Policy header to block inline JavaScript

also define the same headers in middleware during testing and
development to avoid building a container for every change

Diff: diff --git a/etc/nginx.conf b/etc/nginx.conf
index 5e7903145..95a109aaf 100644
--- a/etc/nginx.conf
+++ b/etc/nginx.conf
@@ -60,12 +60,14 @@ http {
 
     upstream kiwitcms {
         server unix:///tmp/kiwitcms.sock;
     }
 
+    # WARNING: make sure these match tcms.core.middleware.ExtraHeadersMiddleware
     add_header X-Frame-Options DENY;
     add_header X-Content-Type-Options nosniff;
+    add_header Content-Security-Policy "script-src 'self' cdn.crowdin.com;";
 
     server {
         listen       8080;
         listen       [::]:8080;
         server_name  _;
diff --git a/tcms/core/middleware.py b/tcms/core/middleware.py
index 593d9c627..1f0e608dc 100644
--- a/tcms/core/middleware.py
+++ b/tcms/core/middleware.py
@@ -16,5 +16,22 @@ class CheckDBStructureExistsMiddleware(MiddlewareMixin):
             Site.objects.get(pk=settings.SITE_ID)
         except (OperationalError, ProgrammingError):
             # Redirect to Setup view
             return HttpResponseRedirect(reverse("init-db"))
         return None
+
+
+class ExtraHeadersMiddleware(MiddlewareMixin):
+    """
+    This is enabled only during testing and development. The actual headers
+    are configured in `etc/nginx.conf`!
+    """
+
+    def process_response(self, request, response):
+        if settings.DEBUG:
+            response.headers["X-Frame-Options"] = "DENY"
+            response.headers["X-Content-Type-Options"] = "nosniff"
+            response.headers[
+                "Content-Security-Policy"
+            ] = "script-src 'self' cdn.crowdin.com;"
+
+        return response
diff --git a/tcms/settings/devel.py b/tcms/settings/devel.py
index 32c3e0371..9060bfd65 100644
--- a/tcms/settings/devel.py
+++ b/tcms/settings/devel.py
@@ -28,11 +28,14 @@ CACHES = {
         "BACKEND": "django.core.cache.backends.dummy.DummyCache",
     }
 }
 # django-debug-toolbar settings
 
-MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]  # noqa: F405
+MIDDLEWARE += [  # noqa: F405
+    "debug_toolbar.middleware.DebugToolbarMiddleware",
+    "tcms.core.middleware.ExtraHeadersMiddleware",
+]
 
 INSTALLED_APPS += ["debug_toolbar"]  # noqa: F405
 
 MEDIA_ROOT = os.path.join(TCMS_ROOT_PATH, "..", "uploads")  # noqa: F405
 
diff --git a/tests/test_http.sh b/tests/test_http.sh
index fbc400f21..6565ed913 100755
--- a/tests/test_http.sh
+++ b/tests/test_http.sh
@@ -85,10 +85,14 @@ _EOF_
 
     rlPhaseStartTest "Should send X-Content-Type-Options header"
         rlRun -t -c "curl -k -D- https://localhost 2>/dev/null | grep 'X-Content-Type-Options: nosniff'"
     rlPhaseEnd
 
+    rlPhaseStartTest "Should send Content-Security-Policy header"
+        rlRun -t -c "curl -k -D- https://localhost 2>/dev/null | grep $'Content-Security-Policy: script-src \'self\' cdn.crowdin.com;'"
+    rlPhaseEnd
+
     rlPhaseStartTest "Performance baseline for /accounts/register/"
         exec_wrk "https://localhost/accounts/login/" "$WRK_DIR" "register-account-page"
     rlPhaseEnd
 
     rlPhaseStartTest "Performance baseline for /accounts/login/"

Vulnerable code: MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]  # noqa: F405
Fixed code:     # WARNING: make sure these match tcms.core.middleware.ExtraHeadersMiddleware
    add_header Content-Security-Policy "script-src 'self' cdn.crowdin.com;";


class ExtraHeadersMiddleware(MiddlewareMixin):
    """
    This is enabled only during testing and development. The actual headers
    are configured in `etc/nginx.conf`!
    """

    def process_response(self, request, response):
        if settings.DEBUG:
            response.headers["X-Frame-Options"] = "DENY"
            response.headers["X-Content-Type-Options"] = "nosniff"
            response.headers[
                "Content-Security-Policy"
            ] = "script-src 'self' cdn.crowdin.com;"

        return response
MIDDLEWARE += [  # noqa: F405
    "debug_toolbar.middleware.DebugToolbarMiddleware",
    "tcms.core.middleware.ExtraHeadersMiddleware",
]
    rlPhaseStartTest "Should send Content-Security-Policy header"
        rlRun -t -c "curl -k -D- https://localhost 2>/dev/null | grep $'Content-Security-Policy: script-src \'self\' cdn.crowdin.com;'"
    rlPhaseEnd