Parse input of search filters to prevent errors based on invalid regex, fixes #113

Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
This commit is contained in:
Yohann D'ANELLO 2022-03-10 16:11:01 +01:00 committed by korenstin
parent b8f81048a5
commit 1a258dfe9e
10 changed files with 93 additions and 38 deletions

View File

@ -1,9 +1,10 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from api.viewsets import ReadProtectedModelViewSet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
from api.filters import RegexSafeSearchFilter
from api.viewsets import ReadProtectedModelViewSet
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer
from ..models import Activity, ActivityType, Entry, Guest
@ -29,7 +30,7 @@ class ActivityViewSet(ReadProtectedModelViewSet):
"""
queryset = Activity.objects.order_by('id')
serializer_class = ActivitySerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club',
'date_start', 'date_end', 'valid', 'open', ]
search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name',
@ -47,7 +48,7 @@ class GuestViewSet(ReadProtectedModelViewSet):
"""
queryset = Guest.objects.order_by('id')
serializer_class = GuestSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name',
'inviter__alias__normalized_name', ]
search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name',
@ -62,7 +63,7 @@ class EntryViewSet(ReadProtectedModelViewSet):
"""
queryset = Entry.objects.order_by('id')
serializer_class = EntrySerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['activity', 'time', 'note', 'guest', ]
search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name',
'$guest__last_name', '$guest__first_name', ]

42
apps/api/filters.py Normal file
View File

@ -0,0 +1,42 @@
import re
from functools import lru_cache
from rest_framework.filters import SearchFilter
class RegexSafeSearchFilter(SearchFilter):
@lru_cache
def validate_regex(self, search_term) -> bool:
try:
re.compile(search_term)
return True
except re.error:
return False
def get_search_fields(self, view, request):
"""
Ensure that given regex are valid.
If not, we consider that the user is trying to search by substring.
"""
search_fields = super().get_search_fields(view, request)
search_terms = self.get_search_terms(request)
for search_term in search_terms:
if not self.validate_regex(search_term):
# Invalid regex. We assume we don't query by regex but by substring.
search_fields = [f.replace('$', '') for f in search_fields]
break
return search_fields
def get_search_terms(self, request):
"""
Ensure that search field is a valid regex query. If not, we remove extra characters.
"""
terms = super().get_search_terms(request)
if not all(self.validate_regex(term) for term in terms):
# Invalid regex. If a ^ is prefixed to the search term, we remove it.
terms = [term[1:] if term[0] == '^' else term for term in terms]
# Same for dollars.
terms = [term[:-1] if term[-1] == '$' else term for term in terms]
return terms

View File

@ -12,11 +12,13 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models.fields.files import ImageFieldFile
from django.test import TestCase
from django_filters.rest_framework import DjangoFilterBackend
from phonenumbers import PhoneNumber
from rest_framework.filters import OrderingFilter
from api.filters import RegexSafeSearchFilter
from member.models import Membership, Club
from note.models import NoteClub, NoteUser, Alias, Note
from permission.models import PermissionMask, Permission, Role
from phonenumbers import PhoneNumber
from rest_framework.filters import SearchFilter, OrderingFilter
from .viewsets import ContentTypeViewSet, UserViewSet
@ -87,7 +89,7 @@ class TestAPI(TestCase):
resp = self.client.get(url + f"?ordering=-{field}")
self.assertEqual(resp.status_code, 200)
if SearchFilter in backends:
if RegexSafeSearchFilter in backends:
# Basic search
for field in viewset.search_fields:
obj = self.fix_note_object(obj, field)

View File

@ -6,11 +6,11 @@ from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Q
from django.conf import settings
from django.contrib.auth.models import User
from rest_framework.filters import SearchFilter
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
from permission.backends import PermissionBackend
from note.models import Alias
from .filters import RegexSafeSearchFilter
from .serializers import UserSerializer, ContentTypeSerializer
@ -107,6 +107,6 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
"""
queryset = ContentType.objects.order_by('id')
serializer_class = ContentTypeSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['id', 'app_label', 'model', ]
search_fields = ['$app_label', '$model', ]

View File

@ -3,6 +3,7 @@
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter
from api.viewsets import ReadOnlyProtectedModelViewSet
from .serializers import ChangelogSerializer

View File

@ -2,7 +2,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.filters import OrderingFilter
from api.filters import RegexSafeSearchFilter
from api.viewsets import ReadProtectedModelViewSet
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
@ -17,7 +19,7 @@ class ProfileViewSet(ReadProtectedModelViewSet):
"""
queryset = Profile.objects.order_by('id')
serializer_class = ProfileSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['user', 'user__first_name', 'user__last_name', 'user__username', 'user__email',
'user__note__alias__name', 'user__note__alias__normalized_name', 'phone_number', "section",
'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration',
@ -34,7 +36,7 @@ class ClubViewSet(ReadProtectedModelViewSet):
"""
queryset = Club.objects.order_by('id')
serializer_class = ClubSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club',
'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid',
'membership_duration', 'membership_start', 'membership_end', ]
@ -49,7 +51,7 @@ class MembershipViewSet(ReadProtectedModelViewSet):
"""
queryset = Membership.objects.order_by('id')
serializer_class = MembershipSerializer
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
filter_backends = [DjangoFilterBackend, OrderingFilter, RegexSafeSearchFilter]
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', 'club__note__alias__normalized_name',
'user__username', 'user__last_name', 'user__first_name', 'user__email',
'user__note__alias__name', 'user__note__alias__normalized_name',

View File

@ -1,15 +1,18 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import re
from django.conf import settings
from django.db.models import Q
from django.core.exceptions import ValidationError
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.filters import OrderingFilter
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status
from api.filters import RegexSafeSearchFilter
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
from permission.backends import PermissionBackend
@ -29,7 +32,7 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
"""
queryset = Note.objects.order_by('id')
serializer_class = NotePolymorphicSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter, OrderingFilter]
filterset_fields = ['alias__name', 'polymorphic_ctype', 'is_active', 'balance', 'last_negative', 'created_at', ]
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model',
'$noteuser__user__last_name', '$noteuser__user__first_name', '$noteuser__user__email',
@ -95,7 +98,7 @@ class AliasViewSet(ReadProtectedModelViewSet):
"""
queryset = Alias.objects
serializer_class = AliasSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
@ -147,7 +150,7 @@ class AliasViewSet(ReadProtectedModelViewSet):
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
queryset = Alias.objects
serializer_class = ConsumerSerializer
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
filter_backends = [RegexSafeSearchFilter, OrderingFilter, DjangoFilterBackend]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
@ -207,7 +210,7 @@ class TemplateCategoryViewSet(ReadProtectedModelViewSet):
"""
queryset = TemplateCategory.objects.order_by('name')
serializer_class = TemplateCategorySerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'templates', 'templates__name']
search_fields = ['$name', '$templates__name', ]
@ -220,7 +223,7 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
"""
queryset = TransactionTemplate.objects.order_by('name')
serializer_class = TransactionTemplateSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
filterset_fields = ['name', 'amount', 'display', 'category', 'category__name', ]
search_fields = ['$name', '$category__name', ]
ordering_fields = ['amount', ]
@ -234,7 +237,7 @@ class TransactionViewSet(ReadProtectedModelViewSet):
"""
queryset = Transaction.objects.order_by('-created_at')
serializer_class = TransactionPolymorphicSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name',
'destination', 'destination_alias', 'destination__alias__name',
'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount',

View File

@ -1,9 +1,10 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from api.viewsets import ReadOnlyProtectedModelViewSet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
from api.filters import RegexSafeSearchFilter
from api.viewsets import ReadOnlyProtectedModelViewSet
from .serializers import PermissionSerializer, RoleSerializer
from ..models import Permission, Role
@ -17,7 +18,7 @@ class PermissionViewSet(ReadOnlyProtectedModelViewSet):
"""
queryset = Permission.objects.order_by('id')
serializer_class = PermissionSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ]
search_fields = ['$model__name', '$query', '$description', ]
@ -30,6 +31,6 @@ class RoleViewSet(ReadOnlyProtectedModelViewSet):
"""
queryset = Role.objects.order_by('id')
serializer_class = RoleSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'permissions', 'for_club', 'memberships__user', ]
search_fields = ['$name', '$for_club__name', ]

View File

@ -2,7 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
from api.filters import RegexSafeSearchFilter
from api.viewsets import ReadProtectedModelViewSet
from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer, \
@ -18,7 +19,7 @@ class InvoiceViewSet(ReadProtectedModelViewSet):
"""
queryset = Invoice.objects.order_by('id')
serializer_class = InvoiceSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['bde', 'object', 'description', 'name', 'address', 'date', 'acquitted', 'locked', ]
search_fields = ['$object', '$description', '$name', '$address', ]
@ -31,7 +32,7 @@ class ProductViewSet(ReadProtectedModelViewSet):
"""
queryset = Product.objects.order_by('invoice_id', 'id')
serializer_class = ProductSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['invoice', 'designation', 'quantity', 'amount', ]
search_fields = ['$designation', '$invoice__object', ]
@ -44,7 +45,7 @@ class RemittanceTypeViewSet(ReadProtectedModelViewSet):
"""
queryset = RemittanceType.objects.order_by('id')
serializer_class = RemittanceTypeSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['note', ]
search_fields = ['$note__special_type', ]
@ -57,7 +58,7 @@ class RemittanceViewSet(ReadProtectedModelViewSet):
"""
queryset = Remittance.objects.order_by('id')
serializer_class = RemittanceSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['date', 'remittance_type', 'comment', 'closed', 'transaction_proxies__transaction', ]
search_fields = ['$remittance_type__note__special_type', '$comment', ]
@ -70,7 +71,7 @@ class SogeCreditViewSet(ReadProtectedModelViewSet):
"""
queryset = SogeCredit.objects.order_by('id')
serializer_class = SogeCreditSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['user', 'user__last_name', 'user__first_name', 'user__email', 'user__note__alias__name',
'user__note__alias__normalized_name', 'transactions', 'credit_transaction', ]
search_fields = ['$user__last_name', '$user__first_name', '$user__email', '$user__note__alias__name',

View File

@ -2,7 +2,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.filters import OrderingFilter
from api.filters import RegexSafeSearchFilter
from api.viewsets import ReadProtectedModelViewSet
from .serializers import WEIClubSerializer, BusSerializer, BusTeamSerializer, WEIRoleSerializer, \
@ -18,7 +20,7 @@ class WEIClubViewSet(ReadProtectedModelViewSet):
"""
queryset = WEIClub.objects.order_by('id')
serializer_class = WEIClubSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'year', 'date_start', 'date_end', 'email', 'note__alias__name',
'note__alias__normalized_name', 'parent_club', 'parent_club__name', 'require_memberships',
'membership_fee_paid', 'membership_fee_unpaid', 'membership_duration', 'membership_start',
@ -34,7 +36,7 @@ class BusViewSet(ReadProtectedModelViewSet):
"""
queryset = Bus.objects.order_by('id')
serializer_class = BusSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'wei', 'description', ]
search_fields = ['$name', '$wei__name', '$description', ]
@ -47,7 +49,7 @@ class BusTeamViewSet(ReadProtectedModelViewSet):
"""
queryset = BusTeam.objects.order_by('id')
serializer_class = BusTeamSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'bus', 'color', 'description', 'bus__wei', ]
search_fields = ['$name', '$bus__name', '$bus__wei__name', '$description', ]
@ -60,7 +62,7 @@ class WEIRoleViewSet(ReadProtectedModelViewSet):
"""
queryset = WEIRole.objects.order_by('id')
serializer_class = WEIRoleSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'permissions', 'memberships', ]
search_fields = ['$name', ]
@ -73,7 +75,7 @@ class WEIRegistrationViewSet(ReadProtectedModelViewSet):
"""
queryset = WEIRegistration.objects.order_by('id')
serializer_class = WEIRegistrationSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['user', 'user__username', 'user__first_name', 'user__last_name', 'user__email',
'user__note__alias__name', 'user__note__alias__normalized_name', 'wei', 'wei__name',
'wei__email', 'wei__year', 'soge_credit', 'caution_check', 'birth_date', 'gender',
@ -92,7 +94,7 @@ class WEIMembershipViewSet(ReadProtectedModelViewSet):
"""
queryset = WEIMembership.objects.order_by('id')
serializer_class = WEIMembershipSerializer
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
filter_backends = [DjangoFilterBackend, OrderingFilter, RegexSafeSearchFilter]
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name',
'club__note__alias__normalized_name', 'user__username', 'user__last_name',
'user__first_name', 'user__email', 'user__note__alias__name',