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()