From b0df124601f990d1cc2bb526a13dace1d99ff9c0 Mon Sep 17 00:00:00 2001 From: catborise Date: Tue, 6 Oct 2026 08:48:51 +0300 Subject: [PATCH] fix(appsettings): SASS_DIR must be a directory inside the project The settings page lists the themes of SASS_DIR and writes wvc-main.scss into it when the theme changes; the value could be any path, so a superuser could make the app list and write files anywhere it may (CodeQL py/path-injection). SASS_DIR is now accepted only if it resolves to an existing directory inside the project (the default is dev/scss). A stored value outside it is not used: the page shows an error and offers no themes, so nothing is written. Installations that keep their SCSS outside the project need to move it under it. The tests fail on the previous code for an absolute path, "../", a path climbing out and a missing directory, and for a stored value outside the project. --- appsettings/test_theme_name.py | 29 +++++++++++++++++++++++++++++ appsettings/views.py | 29 ++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/appsettings/test_theme_name.py b/appsettings/test_theme_name.py index fc9de8d8..bf64a16f 100644 --- a/appsettings/test_theme_name.py +++ b/appsettings/test_theme_name.py @@ -25,3 +25,32 @@ class ThemeNameTests(TestCase): 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) + + +class SassDirTests(TestCase): + """SASS_DIR must be a directory inside the project: the page lists it and + writes wvc-main.scss into it.""" + + def setUp(self): + self.client.force_login(User.objects.create_superuser("sd-admin", "sd@example.com", "pw")) + self.before = AppSettings.objects.get(key="SASS_DIR").value + + def test_a_directory_outside_the_project_is_refused(self): + outside = tempfile.TemporaryDirectory() + self.addCleanup(outside.cleanup) + for value in (outside.name, "/etc", "../", "dev/scss/../../..", "no/such/dir"): + with self.subTest(value=value): + self.client.post(reverse("appsettings"), {"SASS_DIR": value}) + self.assertEqual(AppSettings.objects.get(key="SASS_DIR").value, self.before) + + def test_the_project_scss_directory_is_accepted(self): + self.client.post(reverse("appsettings"), {"SASS_DIR": "dev/scss"}) + self.assertEqual(AppSettings.objects.get(key="SASS_DIR").value, "dev/scss") + + def test_a_stored_directory_outside_the_project_is_not_used(self): + outside = tempfile.TemporaryDirectory() + self.addCleanup(outside.cleanup) + os.makedirs(os.path.join(outside.name, "wvc-themes", "flatly")) + AppSettings.objects.filter(key="SASS_DIR").update(value=outside.name) + self.client.post(reverse("appsettings"), {"BOOTSTRAP_THEME": "flatly"}) + self.assertFalse(os.path.exists(os.path.join(outside.name, "wvc-main.scss"))) diff --git a/appsettings/views.py b/appsettings/views.py index bbcac2f4..7e8d26ed 100644 --- a/appsettings/views.py +++ b/appsettings/views.py @@ -1,7 +1,9 @@ import os +from pathlib import Path import sass from admin.decorators import superuser_only +from django.conf import settings from django.contrib import messages from django.http import HttpResponseRedirect from django.shortcuts import render @@ -11,6 +13,13 @@ from logs.views import addlogmsg from appsettings.models import AppSettings +def sass_dir_in_project(value): + """True if the SASS_DIR value is an existing directory inside the project: + the page lists its themes and writes wvc-main.scss into it.""" + path = (Path(settings.BASE_DIR) / value).resolve() + return path.is_dir() and path.is_relative_to(Path(settings.BASE_DIR).resolve()) + + @superuser_only def appsettings(request): """ @@ -21,11 +30,14 @@ def appsettings(request): 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: - messages.error(request, err) - addlogmsg(request.user.username, "-", "", err) + if not sass_dir_in_project(sass_dir.value): + messages.error(request, _("SASS directory path must be a directory inside the project: %(dir)s") % {"dir": sass_dir.value}) + else: + try: + themes_list = os.listdir(sass_dir.value + "/wvc-themes") + except FileNotFoundError as err: + messages.error(request, err) + addlogmsg(request.user.username, "-", "", err) # Bootstrap settings related with filesystems, because of that they are excluded from other settings appsettings = AppSettings.objects.exclude( @@ -34,6 +46,13 @@ def appsettings(request): if request.method == "POST": if "SASS_DIR" in request.POST: + if not sass_dir_in_project(request.POST.get("SASS_DIR", "")): + messages.error( + request, + _("SASS directory path must be a directory inside the project: %(dir)s") + % {"dir": request.POST.get("SASS_DIR", "")}, + ) + return HttpResponseRedirect(request.get_full_path()) try: sass_dir.value = request.POST.get("SASS_DIR", "") sass_dir.save()