From dde2537fbb04ad78a673092a931b449245a2d6ae Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Sat, 25 Feb 2023 13:09:53 -0500 Subject: [PATCH] Fixed #27397 -- Prevented integer overflows on integer field lookups. This prevents a sqlite3 crash and address a potential DDoS vector on PostgreSQL caused by full-table-scans on overflows. --- django/db/models/lookups.py | 43 ++++++++++++++++++++++--- tests/model_fields/test_integerfield.py | 39 ++++++++++++++++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py index 9e2d9373e6..46ebe3f3a2 100644 --- a/django/db/models/lookups.py +++ b/django/db/models/lookups.py @@ -1,7 +1,7 @@ import itertools import math -from django.core.exceptions import EmptyResultSet +from django.core.exceptions import EmptyResultSet, FullResultSet from django.db.models.expressions import Case, Expression, Func, Value, When from django.db.models.fields import ( BooleanField, @@ -389,6 +389,24 @@ class LessThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup): lookup_name = "lte" +class IntegerFieldOverflow: + underflow_exception = EmptyResultSet + overflow_exception = EmptyResultSet + + def process_rhs(self, compiler, connection): + rhs = self.rhs + if isinstance(rhs, int): + field_internal_type = self.lhs.output_field.get_internal_type() + min_value, max_value = connection.ops.integer_field_range( + field_internal_type + ) + if min_value is not None and rhs < min_value: + raise self.underflow_exception + if max_value is not None and rhs > max_value: + raise self.overflow_exception + return super().process_rhs(compiler, connection) + + class IntegerFieldFloatRounding: """ Allow floats to work as query values for IntegerField. Without this, the @@ -402,13 +420,30 @@ class IntegerFieldFloatRounding: @IntegerField.register_lookup -class IntegerGreaterThanOrEqual(IntegerFieldFloatRounding, GreaterThanOrEqual): +class IntegerFieldExact(IntegerFieldOverflow, Exact): pass @IntegerField.register_lookup -class IntegerLessThan(IntegerFieldFloatRounding, LessThan): - pass +class IntegerGreaterThan(IntegerFieldOverflow, GreaterThan): + underflow_exception = FullResultSet + + +@IntegerField.register_lookup +class IntegerGreaterThanOrEqual( + IntegerFieldOverflow, IntegerFieldFloatRounding, GreaterThanOrEqual +): + underflow_exception = FullResultSet + + +@IntegerField.register_lookup +class IntegerLessThan(IntegerFieldOverflow, IntegerFieldFloatRounding, LessThan): + overflow_exception = FullResultSet + + +@IntegerField.register_lookup +class IntegerLessThanOrEqual(IntegerFieldOverflow, LessThanOrEqual): + overflow_exception = FullResultSet @Field.register_lookup diff --git a/tests/model_fields/test_integerfield.py b/tests/model_fields/test_integerfield.py index 3ee6ac16d8..7698160678 100644 --- a/tests/model_fields/test_integerfield.py +++ b/tests/model_fields/test_integerfield.py @@ -1,3 +1,5 @@ +from unittest import SkipTest + from django.core import validators from django.core.exceptions import ValidationError from django.db import IntegrityError, connection, models @@ -94,6 +96,43 @@ class IntegerFieldTests(TestCase): instance.value = max_value instance.full_clean() + def test_backend_range_min_value_lookups(self): + min_value = self.backend_range[0] + if min_value is None: + raise SkipTest("Backend doesn't define an integer min value.") + underflow_value = min_value - 1 + self.model.objects.create(value=min_value) + # A refresh of obj is necessary because last_insert_id() is bugged + # on MySQL and returns invalid values. + obj = self.model.objects.get(value=min_value) + with self.assertNumQueries(0), self.assertRaises(self.model.DoesNotExist): + self.model.objects.get(value=underflow_value) + with self.assertNumQueries(1): + self.assertEqual(self.model.objects.get(value__gt=underflow_value), obj) + with self.assertNumQueries(1): + self.assertEqual(self.model.objects.get(value__gte=underflow_value), obj) + with self.assertNumQueries(0), self.assertRaises(self.model.DoesNotExist): + self.model.objects.get(value__lt=underflow_value) + with self.assertNumQueries(0), self.assertRaises(self.model.DoesNotExist): + self.model.objects.get(value__lte=underflow_value) + + def test_backend_range_max_value_lookups(self): + max_value = self.backend_range[-1] + if max_value is None: + raise SkipTest("Backend doesn't define an integer max value.") + overflow_value = max_value + 1 + obj = self.model.objects.create(value=max_value) + with self.assertNumQueries(0), self.assertRaises(self.model.DoesNotExist): + self.model.objects.get(value=overflow_value) + with self.assertNumQueries(0), self.assertRaises(self.model.DoesNotExist): + self.model.objects.get(value__gt=overflow_value) + with self.assertNumQueries(0), self.assertRaises(self.model.DoesNotExist): + self.model.objects.get(value__gte=overflow_value) + with self.assertNumQueries(1): + self.assertEqual(self.model.objects.get(value__lt=overflow_value), obj) + with self.assertNumQueries(1): + self.assertEqual(self.model.objects.get(value__lte=overflow_value), obj) + def test_redundant_backend_range_validators(self): """ If there are stricter validators than the ones from the database