CVE-2020-15225 (published 2021-04-29T21:15Z)

django-filter is a generic system for filtering Django QuerySets based on user selections. In django-filter before version 2.4.0, automatically generated `NumberFilter` instances, whose value was later converted to an integer, were subject to potential DoS from maliciously input using exponential format with sufficiently large exponents. Version 2.4.0+ applies a `MaxValueValidator` with a a default `limit_value` of 1e50 to the form field used by `NumberFilter` instances. In addition, `NumberFilter` implements the new `get_max_validator()` which should return a configured validator instance to customise the limit, or else `None` to disable the additional validation. Users may manually apply an equivalent validator if they are not able to upgrade.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:N/I:N/A:P)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-681
cwe_name: Incorrect Conversion between Numeric Types
description: When converting from one data type to another, such as long to integer, data can be omitted or translated in a way that produces unexpected values. If the resulting values are used in a sensitive context, then dangerous behaviors may occur.
extended_description: 
            
            
            
         
         

Fix information:
repo_url: https://github.com/carltongibson/django-filter
commit_details: repo_url: https://github.com/carltongibson/django-filter
author: Carlton Gibson
author_date: 2020-09-27 11:04:03+02:00
msg: Version 2.4 (#1272)

* Added MaxValueValidator to NumberFilter.
* Update docs copyright year.
* Droped using bumpversion.
repository: repo_url: https://github.com/carltongibson/django-filter
repo_name: carltongibson/django-filter
description: A generic system for filtering Django QuerySets based on user selections
repo_language: Python
owner: carltongibson
file_changes: filename: .bumpversion.cfg
change_type: ModificationType.DELETE
diff: @@ -1,24 +0,0 @@
-[bumpversion]
-current_version = 2.3.0
-commit = False
-tag = False
-parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\.(?P<release>[a-z]+)(?P<num>\d+))?
-serialize =
-	{major}.{minor}.{patch}.{release}{num}
-	{major}.{minor}.{patch}
-
-[bumpversion:file:django_filters/__init__.py]
-
-[bumpversion:file:setup.py]
-
-[bumpversion:file:docs/conf.py]
-
-[bumpversion:part:release]
-optional_value = final
-values =
-	dev
-	final
-
-[bumpversion:part:num]
-first_value = 1
-

programming_language: unknown
filename: CHANGES.rst
change_type: ModificationType.MODIFY
diff: @@ -1,3 +1,20 @@
+Version 2.4.0 (2020-9-27)
+--------------------------
+
+* SECURITY: Added a ``MaxValueValidator`` to the form field for
+  ``NumberFilter``. This prevents a potential DoS attack if numbers with very
+  large exponents were subsequently converted to integers.
+
+  The default limit value for the validator is ``1e50``.
+
+  The new ``NumberFilter.get_max_validator()`` allows customising the used
+  validator, and may return ``None`` to disable the validation entirely.
+
+* Added testing against Django 3.1 and Python 3.9.
+
+  In addition tests against Django main development branch are now required to
+  pass.
+
 Version 2.3.0 (2020-6-5)
 ------------------------
 

programming_language: Markdown
filename: __init__.py
change_type: ModificationType.MODIFY
diff: @@ -10,7 +10,7 @@ if pkgutil.find_loader('rest_framework') is not None:
     from . import rest_framework
 del pkgutil
 
-__version__ = '2.3.0'
+__version__ = '2.4.0'
 
 
 def parse_version(version):

programming_language: Python
filename: filters.py
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,7 @@ from collections import OrderedDict
 from datetime import timedelta
 
 from django import forms
+from django.core.validators import MaxValueValidator
 from django.db.models import Q
 from django.db.models.constants import LOOKUP_SEP
 from django.forms.utils import pretty_name
@@ -357,6 +358,23 @@ class ModelMultipleChoiceFilter(QuerySetRequestMixin, MultipleChoiceFilter):
 class NumberFilter(Filter):
     field_class = forms.DecimalField
 
+    def get_max_validator(self):
+        """
+        Return a MaxValueValidator for the field, or None to disable.
+        """
+        return MaxValueValidator(1e50)
+
+    @property
+    def field(self):
+        if not hasattr(self, '_field'):
+            field = super().field
+            max_validator = self.get_max_validator()
+            if max_validator:
+                field.validators.append(max_validator)
+
+            self._field = field
+        return self._field
+
 
 class NumericRangeFilter(Filter):
     field_class = RangeField

programming_language: Python
filename: conf.py
change_type: ModificationType.MODIFY
diff: @@ -13,6 +13,8 @@
 
 import sys, os
 
+from django_filters import __version__
+
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
@@ -41,16 +43,16 @@ master_doc = 'index'
 
 # General information about the project.
 project = u'django-filter'
-copyright = u'2019, Alex Gaynor, Carlton Gibson and others.'
+copyright = u'2020, Alex Gaynor, Carlton Gibson and others.'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
 #
 # The short X.Y version.
-version = '2.3'
+version = __version__
 # The full version, including alpha/beta/rc tags.
-release = '2.3.0'
+release = __version__
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.

programming_language: Python
filename: filters.txt
change_type: ModificationType.MODIFY
diff: @@ -426,6 +426,12 @@ QuerySet, which then gets used as the model's manager::
 Filters based on a numerical value, used with ``IntegerField``, ``FloatField``,
 and ``DecimalField`` by default.
 
+.. method:: NumberFilter.get_max_validator()
+
+    Return a ``MaxValueValidator`` instance that will be added to
+    ``field.validators``. By default uses a limit value of ``1e50``. Return
+    ``None`` to disable maximum value validation.
+
 ``NumericRangeFilter``
 ~~~~~~~~~~~~~~~~~~~~~~
 

programming_language: Markdown
filename: maintainer.txt
change_type: ModificationType.MODIFY
diff: @@ -2,7 +2,6 @@ alabaster==0.7.7
 argh==0.26.1
 Babel==2.2.0
 backports.ssl-match-hostname==3.4.0.2
-bumpversion==0.5.3
 certifi==2015.9.6.2
 docutils==0.12
 funcsigs==0.4

programming_language: Batchfile
filename: setup.py
change_type: ModificationType.MODIFY
diff: @@ -2,12 +2,15 @@ import os
 import sys
 from setuptools import setup, find_packages
 
+# FIXME: Main module requires django to be present, so cannot run setup.py in
+# clean environment.
+# from django_filters import __version__
+__version__ = '2.4.0'
+
 f = open('README.rst')
 readme = f.read()
 f.close()
 
-version = '2.3.0'
-
 if sys.argv[-1] == 'publish':
     if os.system("pip freeze | grep wheel"):
         print("wheel not installed.\nUse `pip install wheel`.\nExiting.")
@@ -18,13 +21,13 @@ if sys.argv[-1] == 'publish':
     os.system("python setup.py sdist bdist_wheel")
     os.system("twine upload dist/*")
     print("You probably want to also tag the version now:")
-    print("  git tag -a %s -m 'version %s'" % (version, version))
+    print("  git tag -a %s -m 'version %s'" % (__version__, __version__))
     print("  git push --tags")
     sys.exit()
 
 setup(
     name='django-filter',
-    version=version,
+    version=__version__,
     description=('Django-filter is a reusable Django application for allowing'
                  ' users to filter querysets dynamically.'),
     long_description=readme,

programming_language: Python
filename: test_forms.py
change_type: ModificationType.MODIFY
diff: @@ -255,3 +255,17 @@ class FilterSetValidityTests(TestCase):
         self.assertFalse(f.is_valid())
         self.assertEqual(f.data, {'price': 'four dollars'})
         self.assertEqual(f.errors, {'price': ['Enter a number.']})
+
+    def test_number_filter_max_value_validation(self):
+        class F(FilterSet):
+            class Meta:
+                model = Book
+                fields = ['average_rating']
+
+        f = F({'average_rating': '1E1001'})
+        self.assertTrue(f.is_bound)
+        self.assertFalse(f.is_valid())
+        self.assertEqual(
+            f.errors,
+            {'average_rating': ['Ensure this value is less than or equal to 1e+50.']}
+        )

programming_language: Python