From dde2537fbb04ad78a673092a931b449245a2d6ae Mon Sep 17 00:00:00 2001
From: Simon Charette <charette.s@gmail.com>
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