From 82bcdc011e2ff674e7dd8fd8cee3a831c908d29b Mon Sep 17 00:00:00 2001
From: Zack Deveau <zack.ref@gmail.com>
Date: Mon, 21 Nov 2022 17:11:31 -0500
Subject: [PATCH] Added integer width check to PostgreSQL::Quoting

Given a value outside the range for a 64bit signed integer type
PostgreSQL will treat the column type as numeric.
Comparing integer values against numeric values can result
in a slow sequential scan.

This behavior is configurable via
ActiveRecord.raise_int_wider_than_64bit which defaults to true.

[CVE-2022-44566]
---
 activerecord-7.0.4/lib/active_record.rb             |  8 ++++++
 .../connection_adapters/postgresql/quoting.rb | 26 +++++++++++++++++
 .../cases/adapters/postgresql/quoting_test.rb | 28 +++++++++++++++++++
 3 files changed, 85 insertions(+)

diff --git a/activerecord-7.0.4/lib/active_record.rb b/activerecord-7.0.4/lib/active_record.rb
index d553fe5c7c..4f6e5493e7 100644
--- a/activerecord-7.0.4/lib/active_record.rb
+++ b/activerecord-7.0.4/lib/active_record.rb
@@ -347,6 +347,14 @@ def self.global_executor_concurrency # :nodoc:
   singleton_class.attr_accessor :use_yaml_unsafe_load
   self.use_yaml_unsafe_load = false
 
+  ##
+  # :singleton-method:
+  # Application configurable boolean that denotes whether or not to raise
+  # an exception when the PostgreSQLAdapter is provided with an integer that
+  # is wider than signed 64bit representation
+  singleton_class.attr_accessor :raise_int_wider_than_64bit
+  self.raise_int_wider_than_64bit = true
+
   ##
   # :singleton-method:
   # Application configurable array that provides additional permitted classes
diff --git a/activerecord-7.0.4/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord-7.0.4/lib/active_record/connection_adapters/postgresql/quoting.rb
index 0d1cd8b02d..d5591dbe00 100644
--- a/activerecord-7.0.4/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord-7.0.4/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -4,6 +4,12 @@
   module ConnectionAdapters
     module PostgreSQL
       module Quoting
+        class IntegerOutOf64BitRange < StandardError
+          def initialize(msg)
+            super(msg)
+          end
+        end
+
         # Escapes binary strings for bytea input to the database.
         def escape_bytea(value)
           @connection.escape_bytea(value) if value
@@ -16,7 +22,27 @@ def unescape_bytea(value)
           @connection.unescape_bytea(value) if value
         end
 
+        def check_int_in_range(value)
+          if value.to_int > 9223372036854775807 || value.to_int < -9223372036854775808
+            exception = <<~ERROR
+              Provided value outside of the range of a signed 64bit integer.
+
+              PostgreSQL will treat the column type in question as a numeric.
+              This may result in a slow sequential scan due to a comparison
+              being performed between an integer or bigint value and a numeric value.
+
+              To allow for this potentially unwanted behavior, set
+              ActiveRecord.raise_int_wider_than_64bit to false.
+            ERROR
+            raise IntegerOutOf64BitRange.new exception
+          end
+        end
+
         def quote(value) # :nodoc:
+          if ActiveRecord.raise_int_wider_than_64bit && value.is_a?(Integer)
+            check_int_in_range(value)
+          end
+
           case value
           when OID::Xml::Data
             "xml '#{quote_string(value.to_s)}'"
diff --git a/test/cases/adapters/postgresql/quoting_test.rb b/test/cases/adapters/postgresql/quoting_test.rb
index d571355a9c..7e01defd96 100644
--- a/test/cases/adapters/postgresql/quoting_test.rb
+++ b/test/cases/adapters/postgresql/quoting_test.rb
@@ -8,6 +8,7 @@
       class QuotingTest < ActiveRecord::PostgreSQLTestCase
         def setup
           @conn = ActiveRecord::Base.connection
+          @raise_int_wider_than_64bit = ActiveRecord.raise_int_wider_than_64bit
         end
 
         def test_type_cast_true
@@ -44,6 +45,33 @@ def test_quote_table_name_with_spaces
           value = "user posts"
           assert_equal "\"user posts\"", @conn.quote_table_name(value)
         end
+
+        def test_raise_when_int_is_wider_than_64bit
+          value = 9223372036854775807 + 1
+          assert_raise ActiveRecord::ConnectionAdapters::PostgreSQL::Quoting::IntegerOutOf64BitRange do
+            @conn.quote(value)
+          end
+
+          value = -9223372036854775808 - 1
+          assert_raise ActiveRecord::ConnectionAdapters::PostgreSQL::Quoting::IntegerOutOf64BitRange do
+            @conn.quote(value)
+          end
+        end
+
+        def test_do_not_raise_when_int_is_not_wider_than_64bit
+          value = 9223372036854775807
+          assert_equal "9223372036854775807", @conn.quote(value)
+
+          value = -9223372036854775808
+          assert_equal "-9223372036854775808", @conn.quote(value)
+        end
+
+        def test_do_not_raise_when_raise_int_wider_than_64bit_is_false
+          ActiveRecord.raise_int_wider_than_64bit = false
+          value = 9223372036854775807 + 1
+          assert_equal "9223372036854775808", @conn.quote(value)
+          ActiveRecord.raise_int_wider_than_64bit = @raise_int_wider_than_64bit
+        end
       end
     end
   end
-- 
2.35.1

