From d816d8d09f0b2d1a2bd46e499e0120162799ecd9 Mon Sep 17 00:00:00 2001 From: catborise Date: Tue, 6 Oct 2026 08:02:05 +0300 Subject: [PATCH] fix(appsettings): the theme must be one of the themes in SASS_DIR The settings page took the posted theme name as it came and wrote it into the @import lines of wvc-main.scss and the compiled CSS: a name with "../" or a quote imported files outside the themes directory (CodeQL py/path-injection; the page is for superusers). The name must now be one of the directories in SASS_DIR/wvc-themes, the list the page already offers; anything else is refused with a message and nothing is written. themes_list is empty when SASS_DIR has no themes, so every name is refused then. Checked with the real dev/scss: flatly is applied and compiled as before, "../../etc" is refused and leaves the CSS alone. The test fails on the previous code. --- appsettings/test_theme_name.py | 27 +++++++++++++++++++++++++++ appsettings/views.py | 5 +++++ 2 files changed, 32 insertions(+) create mode 100644 appsettings/test_theme_name.py diff --git a/appsettings/test_theme_name.py b/appsettings/test_theme_name.py new file mode 100644 index 00000000..fc9de8d8 --- /dev/null +++ b/appsettings/test_theme_name.py @@ -0,0 +1,27 @@ +"""The theme name posted on the settings page must be one of the themes in +SASS_DIR/wvc-themes: it is written into @import paths and the compiled CSS.""" + +import os +import tempfile + +from appsettings.models import AppSettings +from django.contrib.auth.models import User +from django.test import TestCase +from django.urls import reverse + + +class ThemeNameTests(TestCase): + def setUp(self): + self.client.force_login(User.objects.create_superuser("th-admin", "th@example.com", "pw")) + self.sass = tempfile.TemporaryDirectory() + self.addCleanup(self.sass.cleanup) + os.makedirs(os.path.join(self.sass.name, "wvc-themes", "flatly")) + AppSettings.objects.filter(key="SASS_DIR").update(value=self.sass.name) + self.before = AppSettings.objects.get(key="BOOTSTRAP_THEME").value + + def test_a_name_that_is_not_a_theme_is_refused(self): + for theme in ("../../etc", "flatly';@import '/etc/passwd", "nope", ""): + with self.subTest(theme=theme): + self.client.post(reverse("appsettings"), {"BOOTSTRAP_THEME": theme}) + self.assertFalse(os.path.exists(os.path.join(self.sass.name, "wvc-main.scss"))) + self.assertEqual(AppSettings.objects.get(key="BOOTSTRAP_THEME").value, self.before) diff --git a/appsettings/views.py b/appsettings/views.py index e79f926c..bbcac2f4 100644 --- a/appsettings/views.py +++ b/appsettings/views.py @@ -20,6 +20,7 @@ def appsettings(request): main_css = "wvc-main.min.css" sass_dir = AppSettings.objects.get(key="SASS_DIR") bootstrap_theme = AppSettings.objects.get(key="BOOTSTRAP_THEME") + themes_list = [] try: themes_list = os.listdir(sass_dir.value + "/wvc-themes") except FileNotFoundError as err: @@ -50,6 +51,10 @@ def appsettings(request): if "BOOTSTRAP_THEME" in request.POST: theme = request.POST.get("BOOTSTRAP_THEME", "") + # the name goes into @import paths: only the themes in SASS_DIR + if theme not in themes_list: + messages.error(request, _("Unknown theme: %(theme)s") % {"theme": theme}) + return HttpResponseRedirect(request.get_full_path()) scss_var = f"@import '{sass_dir.value}/wvc-themes/{theme}/variables';" # scss_boot = f"@import '{sass_dir.value}/bootstrap/bootstrap.scss';" scss_boot = f"@import '{sass_dir.value}/bootstrap-overrides.scss';"