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..ed2f6c0 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 @@ -89,8 +90,9 @@ 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 not any( + if SECURED_FILTERING and not any( [ (isinstance(f, ChainedManyToManyField) or isinstance(f, ChainedForeignKey)) for f in foreign_model_class._meta.get_fields() @@ -131,8 +133,9 @@ 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 not any( + if SECURED_FILTERING and not any( [ (isinstance(f, ChainedManyToManyField) or isinstance(f, ChainedForeignKey)) for f in foreign_model_class._meta.get_fields() 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"]