From 7f3a5eb00a0aef314a84684d8898ab1866a676b0 Mon Sep 17 00:00:00 2001 From: GabLoyer Date: Thu, 25 May 2023 10:21:38 -0400 Subject: [PATCH 1/2] Add option to disable security check on filtering non opt-in model --- docs/settings.md | 4 ++++ smart_selects/views.py | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/settings.md b/docs/settings.md index f0b3a97..05b3017 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -8,3 +8,7 @@ `USE_DJANGO_JQUERY` : By default, `smart_selects` loads jQuery from Google's CDN. However, it can use jQuery from Django's admin area. Set `USE_DJANGO_JQUERY = True` to enable this behaviour. + +`SMART_SELECTS_SECURED_FILTERING` +: By default, chaining will be prevented if the target model doesn't declare any chained fields in its model. + This option can be disabled (for example if you want to chain only in forms), but please be aware of the security implications. diff --git a/smart_selects/views.py b/smart_selects/views.py index 599b6a8..db81233 100644 --- a/smart_selects/views.py +++ b/smart_selects/views.py @@ -1,4 +1,5 @@ from django.apps import apps +from django.conf import settings from django.core.exceptions import PermissionDenied from django.db.models import Q from django.http import JsonResponse @@ -15,6 +16,7 @@ ) get_model = apps.get_model +SECURED_FILTERING = getattr(settings, "SMART_SELECTS_SECURED_FILTERING", True) def is_m2m(model_class, field): @@ -90,7 +92,7 @@ def filterchain( # SECURITY: Make sure all smart selects requests are opt-in foreign_model_class = get_model(foreign_key_app_name, foreign_key_model_name) - if not any( + if SECURED_FILTERING and not any( [ (isinstance(f, ChainedManyToManyField) or isinstance(f, ChainedForeignKey)) for f in foreign_model_class._meta.get_fields() @@ -132,7 +134,7 @@ def filterchain_all( # SECURITY: Make sure all smart selects requests are opt-in foreign_model_class = get_model(foreign_key_app_name, foreign_key_model_name) - if not any( + if SECURED_FILTERING and not any( [ (isinstance(f, ChainedManyToManyField) or isinstance(f, ChainedForeignKey)) for f in foreign_model_class._meta.get_fields() From 6ce351be6a62ca20c907ce7eb703762340fcbb7c Mon Sep 17 00:00:00 2001 From: GabLoyer Date: Thu, 25 May 2023 10:55:36 -0400 Subject: [PATCH 2/2] Add test case for SECURED_FILTERING settings. --- smart_selects/views.py | 3 ++- test_app/tests.py | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/smart_selects/views.py b/smart_selects/views.py index db81233..ed2f6c0 100644 --- a/smart_selects/views.py +++ b/smart_selects/views.py @@ -16,7 +16,6 @@ ) get_model = apps.get_model -SECURED_FILTERING = getattr(settings, "SMART_SELECTS_SECURED_FILTERING", True) def is_m2m(model_class, field): @@ -91,6 +90,7 @@ def filterchain( keywords = get_keywords(field, value, m2m=m2m) # SECURITY: Make sure all smart selects requests are opt-in + SECURED_FILTERING = getattr(settings, "SMART_SELECTS_SECURED_FILTERING", True) foreign_model_class = get_model(foreign_key_app_name, foreign_key_model_name) if SECURED_FILTERING and not any( [ @@ -133,6 +133,7 @@ def filterchain_all( keywords = get_keywords(field, value) # SECURITY: Make sure all smart selects requests are opt-in + SECURED_FILTERING = getattr(settings, "SMART_SELECTS_SECURED_FILTERING", True) foreign_model_class = get_model(foreign_key_app_name, foreign_key_model_name) if SECURED_FILTERING and not any( [ diff --git a/test_app/tests.py b/test_app/tests.py index 7806179..98a023a 100644 --- a/test_app/tests.py +++ b/test_app/tests.py @@ -1,5 +1,5 @@ from django.apps import apps -from django.test import TestCase, RequestFactory +from django.test import TestCase, RequestFactory, override_settings from django.urls import reverse from .models import Book, Country, Location, Student from smart_selects.views import filterchain, filterchain_all, is_m2m @@ -34,6 +34,22 @@ def test_models_arent_exposed_with_all(self): ) self.assertEqual(response.status_code, 403) + @override_settings(SMART_SELECTS_SECURED_FILTERING=False) + def test_settings_disable_models_are_exposed_with_filter(self): + # If security settings is disabled, all fields are searchable + response = self.client.get( + "/chaining/filter/auth/User/is_superuser/auth/User/password/1/" + ) + self.assertEqual(response.status_code, 200) + + @override_settings(SMART_SELECTS_SECURED_FILTERING=False) + def test_settings_disable_models_are_exposed_with_all(self): + # If security settings is disabled, all fields are searchable + response = self.client.get( + "/chaining/all/auth/User/is_superuser/auth/User/password/1/" + ) + self.assertEqual(response.status_code, 200) + class ViewTests(TestCase): fixtures = ["chained_select", "chained_m2m_select", "grouped_select", "user"]