1
0
mirror of https://github.com/django/django.git synced 2024-12-22 17:16:24 +00:00

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.
This commit is contained in:
Simon Charette 2023-02-25 13:09:53 -05:00 committed by Mariusz Felisiak
parent cc67344db9
commit dde2537fbb
2 changed files with 78 additions and 4 deletions

View File

@ -1,7 +1,7 @@
import itertools import itertools
import math 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.expressions import Case, Expression, Func, Value, When
from django.db.models.fields import ( from django.db.models.fields import (
BooleanField, BooleanField,
@ -389,6 +389,24 @@ class LessThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup):
lookup_name = "lte" 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: class IntegerFieldFloatRounding:
""" """
Allow floats to work as query values for IntegerField. Without this, the Allow floats to work as query values for IntegerField. Without this, the
@ -402,13 +420,30 @@ class IntegerFieldFloatRounding:
@IntegerField.register_lookup @IntegerField.register_lookup
class IntegerGreaterThanOrEqual(IntegerFieldFloatRounding, GreaterThanOrEqual): class IntegerFieldExact(IntegerFieldOverflow, Exact):
pass pass
@IntegerField.register_lookup @IntegerField.register_lookup
class IntegerLessThan(IntegerFieldFloatRounding, LessThan): class IntegerGreaterThan(IntegerFieldOverflow, GreaterThan):
pass 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 @Field.register_lookup

View File

@ -1,3 +1,5 @@
from unittest import SkipTest
from django.core import validators from django.core import validators
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import IntegrityError, connection, models from django.db import IntegrityError, connection, models
@ -94,6 +96,43 @@ class IntegerFieldTests(TestCase):
instance.value = max_value instance.value = max_value
instance.full_clean() 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): def test_redundant_backend_range_validators(self):
""" """
If there are stricter validators than the ones from the database If there are stricter validators than the ones from the database