diff --git a/Matrixfile b/Matrixfile index d3bd308f3ca..4c909082020 100644 --- a/Matrixfile +++ b/Matrixfile @@ -264,7 +264,7 @@ 'redis-3' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ jruby', }, 'appsec:active_record' => { - 'relational_db' => '❌ 2.5 / ❌ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ jruby', + 'relational_db' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ jruby', }, 'appsec:rack' => { 'rack-latest' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ jruby', diff --git a/lib/datadog/appsec/contrib/active_record/instrumentation.rb b/lib/datadog/appsec/contrib/active_record/instrumentation.rb index 0b5ac17191b..267d25f7c3a 100644 --- a/lib/datadog/appsec/contrib/active_record/instrumentation.rb +++ b/lib/datadog/appsec/contrib/active_record/instrumentation.rb @@ -43,7 +43,7 @@ def detect_sql_injection(sql, adapter_name) end end - # patch for all adapters in ActiveRecord >= 7.1 + # patch for mysql2, sqlite3, and postgres+jdbc adapters in ActiveRecord >= 7.1 module InternalExecQueryAdapterPatch def internal_exec_query(sql, *args, **rest) Instrumentation.detect_sql_injection(sql, adapter_name) @@ -52,7 +52,25 @@ def internal_exec_query(sql, *args, **rest) end end - # patch for postgres adapter in ActiveRecord < 7.1 + # patch for mysql2, sqlite3, and postgres+jdbc adapters in ActiveRecord < 7.1 + module ExecQueryAdapterPatch + def exec_query(sql, *args, **rest) + Instrumentation.detect_sql_injection(sql, adapter_name) + + super + end + end + + # patch for mysql2, sqlite3, and postgres+jdbc db adapters in ActiveRecord 4 + module Rails4ExecQueryAdapterPatch + def exec_query(sql, *args) + Instrumentation.detect_sql_injection(sql, adapter_name) + + super + end + end + + # patch for non-jdbc postgres adapter in ActiveRecord > 4 module ExecuteAndClearAdapterPatch def execute_and_clear(sql, *args, **rest) Instrumentation.detect_sql_injection(sql, adapter_name) @@ -61,10 +79,9 @@ def execute_and_clear(sql, *args, **rest) end end - # patch for mysql2 and sqlite3 adapters in ActiveRecord < 7.1 - # this patch is also used when using JDBC adapter - module ExecQueryAdapterPatch - def exec_query(sql, *args, **rest) + # patch for non-jdbc postgres adapter in ActiveRecord 4 + module Rails4ExecuteAndClearAdapterPatch + def execute_and_clear(sql, name, binds) Instrumentation.detect_sql_injection(sql, adapter_name) super diff --git a/lib/datadog/appsec/contrib/active_record/patcher.rb b/lib/datadog/appsec/contrib/active_record/patcher.rb index 7d85036ceec..05ab5e9f081 100644 --- a/lib/datadog/appsec/contrib/active_record/patcher.rb +++ b/lib/datadog/appsec/contrib/active_record/patcher.rb @@ -19,30 +19,81 @@ def target_version end def patch + # Rails 7.0 intruduced new on-load hooks for sqlite3 and postgresql adapters + # The load hook for mysql2 adapter was introduced in Rails 7.1 + # + # If the adapter is not loaded when the :active_record load hook is called, + # we need to add a load hook for the adapter ActiveSupport.on_load :active_record do - instrumentation_module = if ::ActiveRecord.gem_version >= Gem::Version.new('7.1') - Instrumentation::InternalExecQueryAdapterPatch - else - Instrumentation::ExecQueryAdapterPatch - end - if defined?(::ActiveRecord::ConnectionAdapters::SQLite3Adapter) - ::ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend(instrumentation_module) + ::Datadog::AppSec::Contrib::ActiveRecord::Patcher.patch_sqlite3_adapter + else + ActiveSupport.on_load :active_record_sqlite3adapter do + ::Datadog::AppSec::Contrib::ActiveRecord::Patcher.patch_sqlite3_adapter + end end if defined?(::ActiveRecord::ConnectionAdapters::Mysql2Adapter) - ::ActiveRecord::ConnectionAdapters::Mysql2Adapter.prepend(instrumentation_module) + ::Datadog::AppSec::Contrib::ActiveRecord::Patcher.patch_mysql2_adapter + else + ActiveSupport.on_load :active_record_mysql2adapter do + ::Datadog::AppSec::Contrib::ActiveRecord::Patcher.patch_mysql2_adapter + end end if defined?(::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) - unless defined?(::ActiveRecord::ConnectionAdapters::JdbcAdapter) - instrumentation_module = Instrumentation::ExecuteAndClearAdapterPatch + ::Datadog::AppSec::Contrib::ActiveRecord::Patcher.patch_postgresql_adapter + else + ActiveSupport.on_load :active_record_postgresqladapter do + ::Datadog::AppSec::Contrib::ActiveRecord::Patcher.patch_postgresql_adapter end - - ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(instrumentation_module) end end end + + def patch_sqlite3_adapter + instrumentation_module = if ::ActiveRecord.gem_version >= Gem::Version.new('7.1') + Instrumentation::InternalExecQueryAdapterPatch + elsif ::ActiveRecord.gem_version.segments.first == 4 + Instrumentation::Rails4ExecQueryAdapterPatch + else + Instrumentation::ExecQueryAdapterPatch + end + + ::ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend(instrumentation_module) + end + + def patch_mysql2_adapter + instrumentation_module = if ::ActiveRecord.gem_version >= Gem::Version.new('7.1') + Instrumentation::InternalExecQueryAdapterPatch + elsif ::ActiveRecord.gem_version.segments.first == 4 + Instrumentation::Rails4ExecQueryAdapterPatch + else + Instrumentation::ExecQueryAdapterPatch + end + + ::ActiveRecord::ConnectionAdapters::Mysql2Adapter.prepend(instrumentation_module) + end + + def patch_postgresql_adapter + instrumentation_module = if ::ActiveRecord.gem_version.segments.first == 4 + Instrumentation::Rails4ExecuteAndClearAdapterPatch + else + Instrumentation::ExecuteAndClearAdapterPatch + end + + if defined?(::ActiveRecord::ConnectionAdapters::JdbcAdapter) + instrumentation_module = if ::ActiveRecord.gem_version >= Gem::Version.new('7.1') + Instrumentation::InternalExecQueryAdapterPatch + elsif ::ActiveRecord.gem_version.segments.first == 4 + Instrumentation::Rails4ExecQueryAdapterPatch + else + Instrumentation::ExecQueryAdapterPatch + end + end + + ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(instrumentation_module) + end end end end diff --git a/sig/datadog/appsec/contrib/active_record/instrumentation.rbs b/sig/datadog/appsec/contrib/active_record/instrumentation.rbs index daea0a9f684..6f8a9b9d3c8 100644 --- a/sig/datadog/appsec/contrib/active_record/instrumentation.rbs +++ b/sig/datadog/appsec/contrib/active_record/instrumentation.rbs @@ -16,6 +16,14 @@ module Datadog module ExecQueryAdapterPatch def exec_query: (String sql, *untyped args, **untyped rest) -> untyped end + + module Rails4ExecuteAndClearAdapterPatch + def execute_and_clear: (String sql, String name, untyped binds) -> untyped + end + + module Rails4ExecQueryAdapterPatch + def exec_query: (String sql, *untyped args) -> untyped + end end end end diff --git a/sig/datadog/appsec/contrib/active_record/patcher.rbs b/sig/datadog/appsec/contrib/active_record/patcher.rbs index 5741641c5be..f8b96da5233 100644 --- a/sig/datadog/appsec/contrib/active_record/patcher.rbs +++ b/sig/datadog/appsec/contrib/active_record/patcher.rbs @@ -8,6 +8,12 @@ module Datadog def self?.target_version: () -> Gem::Version? def self?.patch: () -> void + + def self?.patch_sqlite3_adapter: () -> void + + def self?.patch_mysql2_adapter: () -> void + + def self?.patch_postgresql_adapter: () -> void end end end