Skip to content

Commit

Permalink
Add ActiveRecord instrumentation to detect SQLi in AppSec
Browse files Browse the repository at this point in the history
  • Loading branch information
y9v committed Nov 21, 2024
1 parent 722f487 commit 6a6d8b2
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 1 deletion.
2 changes: 1 addition & 1 deletion datadog.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'datadog-ruby_core_source', '~> 3.3'

# Used by appsec
spec.add_dependency 'libddwaf', '~> 1.15.0.0.0'
spec.add_dependency 'libddwaf', '~> 1.18.0.0.0'

# When updating the version here, please also update the version in `libdatadog_extconf_helpers.rb`
# (and yes we have a test for it)
Expand Down
1 change: 1 addition & 0 deletions lib/datadog/appsec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def components
end

# Integrations
require_relative 'appsec/contrib/active_record/integration'
require_relative 'appsec/contrib/rack/integration'
require_relative 'appsec/contrib/sinatra/integration'
require_relative 'appsec/contrib/rails/integration'
Expand Down
41 changes: 41 additions & 0 deletions lib/datadog/appsec/contrib/active_record/integration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

require_relative '../integration'
require_relative 'patcher'

module Datadog
module AppSec
module Contrib
module ActiveRecord
# Description of ActiveRecord integration
class Integration
include Datadog::AppSec::Contrib::Integration

MINIMUM_VERSION = Gem::Version.new('4')

register_as :active_record, auto_patch: false

def self.version
Gem.loaded_specs['activerecord'] && Gem.loaded_specs['activerecord'].version
end

def self.loaded?
!defined?(::ActiveRecord).nil?
end

def self.compatible?
super && version >= MINIMUM_VERSION
end

def self.auto_instrument?
true
end

def patcher
Patcher
end
end
end
end
end
end
39 changes: 39 additions & 0 deletions lib/datadog/appsec/contrib/active_record/mysql2_adapter_patch.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module Datadog
module AppSec
module Contrib
module ActiveRecord
# AppSec module that will be prepended to ActiveRecord adapter
module Mysql2AdapterPatch
def internal_exec_query(sql, *args, **rest)
scope = AppSec.active_scope
return super unless scope

ephemeral_data = {
'server.db.statement' => sql,
'server.db.system' => 'mysql'
}

result = scope.processor_context.run({}, ephemeral_data, Datadog.configuration.appsec.waf_timeout)

if result.status == :match
Datadog::AppSec::Event.tag_and_keep!(scope, result)

event = {
waf_result: result,
trace: scope.trace,
span: scope.service_entry_span,
sql: sql,
actions: result.actions
}
scope.processor_context.events << event
end

super
end
end
end
end
end
end
45 changes: 45 additions & 0 deletions lib/datadog/appsec/contrib/active_record/patcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

require_relative '../patcher'
require_relative 'sqlite3_adapter_patch'
require_relative 'postgresql_adapter_patch'
require_relative 'mysql2_adapter_patch'

module Datadog
module AppSec
module Contrib
module ActiveRecord
# AppSec patcher module for ActiveRecord
module Patcher
include Datadog::AppSec::Contrib::Patcher

module_function

def patched?
Patcher.instance_variable_get(:@patched)
end

def target_version
Integration.version
end

def patch
ActiveSupport.on_load :active_record do
if defined? ::ActiveRecord::ConnectionAdapters::SQLite3Adapter
::ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend(SQLite3AdapterPatch)
end

if defined? ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(PostgreSQLAdapterPatch)
end

if defined? ::ActiveRecord::ConnectionAdapters::Mysql2Adapter
::ActiveRecord::ConnectionAdapters::Mysql2Adapter.prepend(Mysql2AdapterPatch)
end
end
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module Datadog
module AppSec
module Contrib
module ActiveRecord
# AppSec module that will be prepended to ActiveRecord adapter
module PostgreSQLAdapterPatch
def internal_exec_query(sql, *args, **rest)
scope = AppSec.active_scope
return super unless scope

ephemeral_data = {
'server.db.statement' => sql,
'server.db.system' => 'postgresql'
}

result = scope.processor_context.run({}, ephemeral_data, Datadog.configuration.appsec.waf_timeout)

if result.status == :match
Datadog::AppSec::Event.tag_and_keep!(scope, result)

event = {
waf_result: result,
trace: scope.trace,
span: scope.service_entry_span,
actions: result.actions,
sql: sql
}
scope.processor_context.events << event
end

super
end
end
end
end
end
end
40 changes: 40 additions & 0 deletions lib/datadog/appsec/contrib/active_record/sqlite3_adapter_patch.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module Datadog
module AppSec
module Contrib
module ActiveRecord
# AppSec module that will be prepended to ActiveRecord adapter
module SQLite3AdapterPatch
def internal_exec_query(sql, *args, **rest)
scope = AppSec.active_scope
return super unless scope

ephemeral_data = {
'server.db.statement' => sql,
'server.db.system' => 'sqlite'
}

waf_timeout = Datadog.configuration.appsec.waf_timeout
result = scope.processor_context.run({}, ephemeral_data, waf_timeout)

if result.status == :match
Datadog::AppSec::Event.tag_and_keep!(scope, result)

event = {
waf_result: result,
trace: scope.trace,
span: scope.service_entry_span,
sql: sql,
actions: result.actions
}
scope.processor_context.events << event
end

super
end
end
end
end
end
end

0 comments on commit 6a6d8b2

Please sign in to comment.