contrast-agent 4.9.1 → 4.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/.rspec_parallel +6 -0
- data/ext/cs__contrast_patch/cs__contrast_patch.c +0 -1
- data/ext/cs__contrast_patch/cs__contrast_patch.h +0 -2
- data/lib/contrast/agent/assess/contrast_event.rb +0 -1
- data/lib/contrast/agent/assess/finalizers/hash.rb +0 -1
- data/lib/contrast/agent/assess/policy/patcher.rb +0 -1
- data/lib/contrast/agent/assess/policy/policy_scanner.rb +0 -2
- data/lib/contrast/agent/assess/policy/preshift.rb +8 -5
- data/lib/contrast/agent/assess/policy/propagation_method.rb +100 -57
- data/lib/contrast/agent/assess/policy/propagator/database_write.rb +0 -2
- data/lib/contrast/agent/assess/policy/propagator/match_data.rb +31 -11
- data/lib/contrast/agent/assess/policy/propagator/split.rb +3 -2
- data/lib/contrast/agent/assess/policy/propagator/substitution.rb +1 -0
- data/lib/contrast/agent/assess/policy/rewriter_patch.rb +0 -1
- data/lib/contrast/agent/assess/policy/source_method.rb +13 -17
- data/lib/contrast/agent/assess/policy/trigger/xpath.rb +0 -1
- data/lib/contrast/agent/assess/policy/trigger_method.rb +59 -83
- data/lib/contrast/agent/assess/property/evented.rb +2 -1
- data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +0 -1
- data/lib/contrast/agent/disable_reaction.rb +1 -1
- data/lib/contrast/agent/exclusion_matcher.rb +0 -4
- data/lib/contrast/agent/inventory/database_config.rb +117 -0
- data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +5 -4
- data/lib/contrast/agent/inventory/policy/datastores.rb +2 -2
- data/lib/contrast/agent/middleware.rb +1 -0
- data/lib/contrast/agent/patching/policy/after_load_patch.rb +3 -0
- data/lib/contrast/agent/patching/policy/after_load_patcher.rb +18 -12
- data/lib/contrast/agent/patching/policy/module_policy.rb +2 -4
- data/lib/contrast/agent/patching/policy/patch.rb +5 -0
- data/lib/contrast/agent/patching/policy/patch_status.rb +3 -7
- data/lib/contrast/agent/patching/policy/patcher.rb +8 -8
- data/lib/contrast/agent/protect/policy/applies_no_sqli_rule.rb +1 -1
- data/lib/contrast/agent/protect/rule/no_sqli.rb +7 -53
- data/lib/contrast/agent/protect/rule/sql_sample_builder.rb +137 -0
- data/lib/contrast/agent/protect/rule/sqli.rb +7 -70
- data/lib/contrast/agent/reaction_processor.rb +1 -1
- data/lib/contrast/agent/request.rb +5 -2
- data/lib/contrast/agent/request_context.rb +19 -22
- data/lib/contrast/agent/static_analysis.rb +1 -1
- data/lib/contrast/agent/tracepoint_hook.rb +6 -1
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/api/communication/messaging_queue.rb +12 -6
- data/lib/contrast/api/communication/service_lifecycle.rb +4 -1
- data/lib/contrast/api/communication/socket_client.rb +4 -4
- data/lib/contrast/api/decorators/agent_startup.rb +4 -4
- data/lib/contrast/api/decorators/application_startup.rb +6 -5
- data/lib/contrast/api/decorators/route_coverage.rb +24 -1
- data/lib/contrast/components/agent.rb +5 -2
- data/lib/contrast/components/assess.rb +6 -3
- data/lib/contrast/components/base.rb +2 -2
- data/lib/contrast/components/config.rb +1 -0
- data/lib/contrast/components/contrast_service.rb +4 -2
- data/lib/contrast/components/logger.rb +13 -8
- data/lib/contrast/components/scope.rb +9 -28
- data/lib/contrast/config/base_configuration.rb +14 -6
- data/lib/contrast/configuration.rb +19 -15
- data/lib/contrast/extension/assess/array.rb +1 -11
- data/lib/contrast/extension/assess/eval_trigger.rb +0 -20
- data/lib/contrast/extension/assess/fiber.rb +0 -11
- data/lib/contrast/extension/assess/hash.rb +0 -10
- data/lib/contrast/extension/assess/kernel.rb +1 -10
- data/lib/contrast/extension/assess/marshal.rb +3 -11
- data/lib/contrast/extension/assess/regexp.rb +0 -11
- data/lib/contrast/extension/assess/string.rb +1 -26
- data/lib/contrast/extension/extension.rb +61 -0
- data/lib/contrast/extension/protect/kernel.rb +0 -10
- data/lib/contrast/framework/grape/support.rb +174 -0
- data/lib/contrast/framework/manager.rb +42 -6
- data/lib/contrast/framework/rack/support.rb +1 -1
- data/lib/contrast/framework/rails/patch/assess_configuration.rb +0 -1
- data/lib/contrast/framework/rails/patch/support.rb +6 -3
- data/lib/contrast/framework/rails/railtie.rb +1 -1
- data/lib/contrast/framework/rails/rewrite/active_record_named.rb +1 -0
- data/lib/contrast/framework/rails/support.rb +60 -13
- data/lib/contrast/framework/sinatra/support.rb +1 -1
- data/lib/contrast/logger/log.rb +89 -15
- data/lib/contrast/utils/io_util.rb +1 -1
- data/lib/contrast/utils/ruby_ast_rewriter.rb +16 -13
- data/lib/contrast/utils/tag_util.rb +2 -1
- data/resources/assess/policy.json +197 -2
- data/resources/deadzone/policy.json +10 -0
- data/ruby-agent.gemspec +10 -1
- metadata +78 -12
- data/lib/contrast/utils/inventory_util.rb +0 -113
@@ -76,11 +76,12 @@ module Contrast
|
|
76
76
|
return unless enabled?
|
77
77
|
return unless activity
|
78
78
|
|
79
|
-
#
|
79
|
+
# Disconnect gemdigest_cache and replace it with an empty one; synch so new libs cannot be added between the
|
80
|
+
# assignment and the replace
|
80
81
|
gem_spec_digest_to_files = @lock.synchronize do
|
81
|
-
|
82
|
-
@gemdigest_cache.
|
83
|
-
|
82
|
+
hold = @gemdigest_cache
|
83
|
+
@gemdigest_cache = Hash.new { |hash, key| hash[key] = Set.new }
|
84
|
+
hold
|
84
85
|
end
|
85
86
|
|
86
87
|
gem_spec_digest_to_files.each_pair do |digest, files|
|
@@ -2,7 +2,7 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require 'contrast/components/logger'
|
5
|
-
require 'contrast/
|
5
|
+
require 'contrast/agent/inventory/database_config'
|
6
6
|
|
7
7
|
module Contrast
|
8
8
|
module Agent
|
@@ -42,7 +42,7 @@ module Contrast
|
|
42
42
|
context.activity.query_count += 1
|
43
43
|
return unless context.activity.query_count == 1
|
44
44
|
|
45
|
-
Contrast::
|
45
|
+
Contrast::Agent::Inventory::DatabaseConfig.append_db_config(context.activity)
|
46
46
|
end
|
47
47
|
end
|
48
48
|
end
|
@@ -59,7 +59,10 @@ module Contrast
|
|
59
59
|
end
|
60
60
|
|
61
61
|
def instrument!
|
62
|
+
return if instrumenting_module == :'Contrast::Framework::Rails::Rewrite' && RAILS_VER >= '2.6.0'
|
63
|
+
|
62
64
|
require instrumentation_file_path
|
65
|
+
|
63
66
|
if instrumenting_module
|
64
67
|
mod = Module.cs__const_get(instrumenting_module)
|
65
68
|
with_contrast_scope { mod.instrument } if mod
|
@@ -4,6 +4,7 @@
|
|
4
4
|
require 'contrast/agent/patching/policy/after_load_patch'
|
5
5
|
require 'contrast/components/logger'
|
6
6
|
require 'contrast/framework/manager'
|
7
|
+
require 'contrast/extension/extension'
|
7
8
|
|
8
9
|
module Contrast
|
9
10
|
module Agent
|
@@ -29,18 +30,23 @@ module Contrast
|
|
29
30
|
# extensions.
|
30
31
|
def apply_direct_patches!
|
31
32
|
@_apply_direct_patches ||= begin
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
33
|
+
paths = %w[
|
34
|
+
array
|
35
|
+
basic_object
|
36
|
+
module
|
37
|
+
fiber_track
|
38
|
+
hash
|
39
|
+
kernel
|
40
|
+
marshal_module
|
41
|
+
regexp
|
42
|
+
string
|
43
|
+
string_interpolation26
|
44
|
+
].cs__freeze
|
45
|
+
paths.each do |p|
|
46
|
+
path_part = "cs__assess_#{ p }"
|
47
|
+
Contrast::Extension::Assess::InstrumentHelper.instrument "#{ path_part }/#{ path_part }"
|
48
|
+
end
|
49
|
+
Contrast::Extension::Assess::InstrumentHelper.instrument 'cs__protect_kernel/cs__protect_kernel'
|
44
50
|
true
|
45
51
|
end
|
46
52
|
end
|
@@ -12,11 +12,9 @@ module Contrast
|
|
12
12
|
# rather than new.
|
13
13
|
class ModulePolicy
|
14
14
|
class << self
|
15
|
-
# Given the name of a module, create a :ModulePolicy for it using
|
16
|
-
# the Policy of each supported feature
|
15
|
+
# Given the name of a module, create a :ModulePolicy for it using the Policy of each supported feature.
|
17
16
|
#
|
18
|
-
# @param module_name [String] the name of the module to which the
|
19
|
-
# policy applies
|
17
|
+
# @param module_name [String] the name of the module to which the policy applies.
|
20
18
|
# @return [Contrast::Agent::Patching::Policy::ModulePolicy]
|
21
19
|
def create_module_policy module_name
|
22
20
|
module_policy = Contrast::Agent::Patching::Policy::ModulePolicy.new
|
@@ -290,6 +290,11 @@ module Contrast
|
|
290
290
|
# we've already patched this class, don't do it again
|
291
291
|
return true if methods.include?(cs_method_name)
|
292
292
|
|
293
|
+
# that method is within Contrast definition so it should be skipped
|
294
|
+
method = mod.instance_method(method_policy.method_name) if method_policy.instance_method
|
295
|
+
method = mod.singleton_method(method_policy.method_name) unless method_policy.instance_method
|
296
|
+
return true if method.owner <= Contrast
|
297
|
+
|
293
298
|
begin
|
294
299
|
contrast_define_method(mod, method_policy, cs_method_name)
|
295
300
|
rescue NameError => e
|
@@ -15,13 +15,9 @@ module Contrast
|
|
15
15
|
# @param mod [Module] the Module for which the status is asked
|
16
16
|
# @return [Contrast::Agent::Patching::Policy::PatchStatus]
|
17
17
|
def get_status mod
|
18
|
-
if mod.cs__const_defined?(status_key, false)
|
19
|
-
|
20
|
-
|
21
|
-
s = new
|
22
|
-
mod.cs__const_set(status_key, s)
|
23
|
-
s
|
24
|
-
end
|
18
|
+
return mod.cs__const_get(status_key) if mod.cs__const_defined?(status_key, false)
|
19
|
+
|
20
|
+
mod.cs__const_set(status_key, new)
|
25
21
|
end
|
26
22
|
|
27
23
|
# Allows our C patches to look up the :MethodPolicy for a given
|
@@ -54,7 +54,8 @@ module Contrast
|
|
54
54
|
def patch
|
55
55
|
catchup_after_load_patches
|
56
56
|
catchup_loaded_methods
|
57
|
-
|
57
|
+
# TODO: RUBY-714 remove guard w/ EOL of 2.5
|
58
|
+
Contrast::Agent::Assess::Policy::RewriterPatch.rewrite_interpolations if RUBY_VERSION < '2.6.0'
|
58
59
|
end
|
59
60
|
|
60
61
|
# Hook to only monkeypatch Contrast. This will not trigger any
|
@@ -86,7 +87,7 @@ module Contrast
|
|
86
87
|
|
87
88
|
load_patches_for_module(mod_name)
|
88
89
|
|
89
|
-
return
|
90
|
+
return if all_module_names.none?(mod_name)
|
90
91
|
|
91
92
|
module_data = Contrast::Agent::ModuleData.new(mod, mod_name)
|
92
93
|
patch_into_module(module_data)
|
@@ -176,12 +177,13 @@ module Contrast
|
|
176
177
|
# @param redo_patch [Boolean] a trigger to force patching regardless of the state of the
|
177
178
|
# Contrast::Agent::Patching::Policy::PatchStatus status on the Module
|
178
179
|
def patch_into_module module_data, redo_patch = false
|
179
|
-
status =
|
180
|
+
status = Contrast::Agent::Patching::Policy::PatchStatus.get_status(module_data.mod)
|
180
181
|
return if (status&.patched? || status&.patching?) && !redo_patch
|
181
182
|
|
182
183
|
# Begin patching our sources into the given module. Any patcher that has the name of the module will be
|
183
184
|
# evaluated for patching. Find all the patchers that apply to this class, sorted by type.
|
184
185
|
module_policy = Contrast::Agent::Patching::Policy::ModulePolicy.create_module_policy(module_data.mod_name)
|
186
|
+
|
185
187
|
# If there's nothing to match, then set that status and exit
|
186
188
|
if module_policy.empty?
|
187
189
|
status.no_patch!
|
@@ -191,8 +193,8 @@ module Contrast
|
|
191
193
|
status.patching!
|
192
194
|
num_applied_patches = patch_into_instance_methods(module_data, module_policy)
|
193
195
|
num_applied_patches += patch_into_singleton_methods(module_data, module_policy)
|
194
|
-
if adjust_for_prepend(module_data) || module_policy.num_expected_patches == num_applied_patches
|
195
196
|
|
197
|
+
if adjust_for_prepend(module_data) || module_policy.num_expected_patches == num_applied_patches
|
196
198
|
status.patched!
|
197
199
|
else
|
198
200
|
status.partial_patch!
|
@@ -258,8 +260,7 @@ module Contrast
|
|
258
260
|
patch_into_methods(mod, methods, module_policy, false)
|
259
261
|
end
|
260
262
|
|
261
|
-
# We've found the patchers that apply to this class (or module). Now we'll
|
262
|
-
# filter on the given method.
|
263
|
+
# We've found the patchers that apply to this class (or module). Now we'll filter on the given method.
|
263
264
|
#
|
264
265
|
# @param mod [Module] The module from which to retrieve instance methods.
|
265
266
|
# @param methods [Array<Symbol>] The names of all the methods in in this module
|
@@ -276,8 +277,7 @@ module Contrast
|
|
276
277
|
is_instance_method)
|
277
278
|
next if method_policy.empty?
|
278
279
|
|
279
|
-
|
280
|
-
count += 1 if patched
|
280
|
+
count += 1 if patch_method(mod, methods, method_policy)
|
281
281
|
end
|
282
282
|
count
|
283
283
|
end
|
@@ -14,7 +14,7 @@ module Contrast
|
|
14
14
|
# infilter methods of the rule should be invoked.
|
15
15
|
module AppliesNoSqliRule
|
16
16
|
extend Contrast::Agent::Protect::Policy::RuleApplicator
|
17
|
-
|
17
|
+
DATABASE_NOSQL = 'MongoDB'
|
18
18
|
class << self
|
19
19
|
def invoke method, _exception, properties, _object, args
|
20
20
|
return unless valid_input?(args)
|
@@ -2,6 +2,7 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require 'contrast/agent/protect/rule/base_service'
|
5
|
+
require 'contrast/agent/protect/rule/sql_sample_builder'
|
5
6
|
|
6
7
|
module Contrast
|
7
8
|
module Agent
|
@@ -9,6 +10,12 @@ module Contrast
|
|
9
10
|
module Rule
|
10
11
|
# The Ruby implementation of the Protect NoSQL Injection rule.
|
11
12
|
class NoSqli < Contrast::Agent::Protect::Rule::BaseService
|
13
|
+
# Generate a sample for the No-SQL injection detection rule, allowing for reporting to and rendering
|
14
|
+
# by TeamServer
|
15
|
+
include SqlSampleBuilder::NoSqliSample
|
16
|
+
# Defining build_attack_with_match method
|
17
|
+
include SqlSampleBuilder::AttackBuilder
|
18
|
+
|
12
19
|
NAME = 'nosql-injection'
|
13
20
|
BLOCK_MESSAGE = 'NoSQLi rule triggered. Response blocked.'
|
14
21
|
|
@@ -31,40 +38,6 @@ module Contrast
|
|
31
38
|
raise Contrast::SecurityException.new(self, BLOCK_MESSAGE) if blocked?
|
32
39
|
end
|
33
40
|
|
34
|
-
def build_attack_with_match context, input_analysis_result, result, query_string, **kwargs
|
35
|
-
if mode == Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION ||
|
36
|
-
mode == Contrast::Api::Settings::ProtectionRule::Mode::PERMIT
|
37
|
-
|
38
|
-
return result
|
39
|
-
end
|
40
|
-
|
41
|
-
attack_string = input_analysis_result.value
|
42
|
-
regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE)
|
43
|
-
return unless query_string.match?(regexp)
|
44
|
-
|
45
|
-
scanner = select_scanner
|
46
|
-
ss = StringScanner.new(query_string)
|
47
|
-
length = attack_string.length
|
48
|
-
while ss.scan_until(regexp)
|
49
|
-
# the pos of StringScanner is at the end of the regexp (input string),
|
50
|
-
# we need the beginning
|
51
|
-
idx = ss.pos - attack_string.length
|
52
|
-
last_boundary, boundary = scanner.crosses_boundary(query_string, idx, input_analysis_result.value)
|
53
|
-
next unless last_boundary && boundary
|
54
|
-
|
55
|
-
kwargs[:start_idx] = idx
|
56
|
-
kwargs[:end_idx] = idx + length
|
57
|
-
kwargs[:boundary_overrun_idx] = boundary
|
58
|
-
kwargs[:input_boundary_idx] = last_boundary
|
59
|
-
|
60
|
-
result ||= build_attack_result(context)
|
61
|
-
update_successful_attack_response(context, input_analysis_result, result, query_string)
|
62
|
-
append_sample(context, input_analysis_result, result, query_string, **kwargs)
|
63
|
-
end
|
64
|
-
|
65
|
-
result
|
66
|
-
end
|
67
|
-
|
68
41
|
protected
|
69
42
|
|
70
43
|
def find_attacker context, potential_attack_string, **kwargs
|
@@ -79,25 +52,6 @@ module Contrast
|
|
79
52
|
end
|
80
53
|
super(context, potential_attack_string, **kwargs)
|
81
54
|
end
|
82
|
-
|
83
|
-
def build_sample context, input_analysis_result, candidate_string, **kwargs
|
84
|
-
input = input_analysis_result.value
|
85
|
-
|
86
|
-
sample = build_base_sample(context, input_analysis_result)
|
87
|
-
sample.no_sqli = Contrast::Api::Dtm::NoSqlInjectionDetails.new
|
88
|
-
sample.no_sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
|
89
|
-
sample.no_sqli.start_idx = sample.no_sqli.query.index(input).to_i
|
90
|
-
sample.no_sqli.boundary_overrun_idx = sample.no_sqli.start_idx + input.length
|
91
|
-
sample.no_sqli.input_boundary_idx = kwargs[:boundary].to_i
|
92
|
-
sample.no_sqli.input_boundary_idx = kwargs[:last_boundary].to_i
|
93
|
-
sample
|
94
|
-
end
|
95
|
-
|
96
|
-
private
|
97
|
-
|
98
|
-
def select_scanner
|
99
|
-
@_select_scanner ||= Contrast::Agent::Protect::Rule::NoSqli::MongoNoSqlScanner.new
|
100
|
-
end
|
101
55
|
end
|
102
56
|
end
|
103
57
|
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'contrast/agent/protect/rule/base'
|
5
|
+
require 'contrast/agent/protect/rule/base_service'
|
6
|
+
|
7
|
+
module Contrast
|
8
|
+
module Agent
|
9
|
+
module Protect
|
10
|
+
module Rule
|
11
|
+
module SqlSampleBuilder
|
12
|
+
# Generate a sample for the SQL injection detection rule, allowing for reporting to and rendering
|
13
|
+
# by TeamServer
|
14
|
+
#
|
15
|
+
# @param context [Contrast::Agent::RequestContext] the context for the current request
|
16
|
+
# @param input_analysis_result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule,
|
17
|
+
# if one exists, in the case of multiple inputs being found to violate the protection criteria
|
18
|
+
# @candidate_string [String] the value of the input which may be an attack
|
19
|
+
# @kwargs [Hash] key - value pairs of context individual rules need to build out details
|
20
|
+
# to send to the Service to tell the story of the attack
|
21
|
+
# @return [Contrast::Api::Dtm::RaspRuleSample] the sample from this attack
|
22
|
+
module SqliSample
|
23
|
+
def build_sample context, input_analysis_result, candidate_string, **kwargs
|
24
|
+
sqli_sample = build_base_sample(context, input_analysis_result)
|
25
|
+
sqli_sample.sqli = Contrast::Api::Dtm::SqlInjectionDetails.new
|
26
|
+
sqli_sample.sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
|
27
|
+
sqli_sample.sqli.start_idx = kwargs[:start_idx]
|
28
|
+
sqli_sample.sqli.end_idx = kwargs[:end_idx]
|
29
|
+
sqli_sample.sqli.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i
|
30
|
+
sqli_sample.sqli.input_boundary_idx = kwargs[:input_boundary_idx].to_i
|
31
|
+
sqli_sample
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Generate a sample for the No-SQL injection detection rule, allowing for reporting to and rendering
|
36
|
+
# by TeamServer
|
37
|
+
#
|
38
|
+
# @param context [Contrast::Agent::RequestContext] the context for the current request
|
39
|
+
# @param input_analysis_result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule,
|
40
|
+
# if one exists, in the case of multiple inputs being found to violate the protection criteria
|
41
|
+
# @candidate_string [String] the value of the input which may be an attack
|
42
|
+
# @kwargs [Hash] key - value pairs of context individual rules need to build out details
|
43
|
+
# to send to the Service to tell the story of the attack
|
44
|
+
# @return [Contrast::Api::Dtm::RaspRuleSample] the sample from this attack
|
45
|
+
module NoSqliSample
|
46
|
+
def build_sample context, input_analysis_result, candidate_string, **kwargs
|
47
|
+
no_sqli_sample = build_base_sample(context, input_analysis_result)
|
48
|
+
no_sqli_sample.no_sqli = Contrast::Api::Dtm::NoSqlInjectionDetails.new
|
49
|
+
no_sqli_sample.no_sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
|
50
|
+
no_sqli_sample.no_sqli.start_idx = kwargs[:start_idx].to_i
|
51
|
+
no_sqli_sample.no_sqli.end_idx = kwargs[:end_idx].to_i
|
52
|
+
no_sqli_sample.no_sqli.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i
|
53
|
+
no_sqli_sample.no_sqli.input_boundary_idx = kwargs[:input_boundary_idx].to_i
|
54
|
+
no_sqli_sample
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# This Module is how we apply the attack fo NoSQL and SQL Injection rule.
|
59
|
+
# It includes methods for building attack with match and database scanners
|
60
|
+
module AttackBuilder
|
61
|
+
# Set up an attack result and assigns Database scanner for the No-SQL and SQLI injection detection rules
|
62
|
+
#
|
63
|
+
# @param context [Contrast::Agent::RequestContext] the context for the current request
|
64
|
+
# @param input_analysis_result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule,
|
65
|
+
# if one exists, in the case of multiple inputs being found to violate the protection criteria
|
66
|
+
# @param result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule, if one exists,
|
67
|
+
# in the case of multiple inputs being found to violate the protection criteria
|
68
|
+
# @query_string [string] he value of the input which may be an attack
|
69
|
+
# @kwargs [Hash] key - value pairs of context individual rules need to build out details to send
|
70
|
+
# to the Service to tell the story of the attack
|
71
|
+
# @return [Contrast::Api::Dtm::AttackResult] the result from this attack
|
72
|
+
def build_attack_with_match context, input_analysis_result, result, query_string, **kwargs
|
73
|
+
if mode == Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION ||
|
74
|
+
mode == Contrast::Api::Settings::ProtectionRule::Mode::PERMIT
|
75
|
+
|
76
|
+
return result
|
77
|
+
end
|
78
|
+
|
79
|
+
attack_string = input_analysis_result.value
|
80
|
+
regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE)
|
81
|
+
|
82
|
+
return unless query_string.match?(regexp)
|
83
|
+
|
84
|
+
database = kwargs[:database]
|
85
|
+
scanner = select_scanner(database)
|
86
|
+
ss = StringScanner.new(query_string)
|
87
|
+
length = attack_string.length
|
88
|
+
while ss.scan_until(regexp)
|
89
|
+
# the pos of StringScanner is at the end of the regexp (input string),
|
90
|
+
# we need the beginning
|
91
|
+
idx = ss.pos - attack_string.length
|
92
|
+
last_boundary, boundary = scanner.crosses_boundary(query_string, idx, input_analysis_result.value)
|
93
|
+
next unless last_boundary && boundary
|
94
|
+
|
95
|
+
result ||= build_attack_result(context)
|
96
|
+
|
97
|
+
record_match(idx, length, boundary, last_boundary, kwargs)
|
98
|
+
append_match(context, input_analysis_result, result, query_string, **kwargs)
|
99
|
+
end
|
100
|
+
|
101
|
+
result
|
102
|
+
end
|
103
|
+
|
104
|
+
def select_scanner database
|
105
|
+
@scanners ||= {
|
106
|
+
Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_MYSQL =>
|
107
|
+
Contrast::Agent::Protect::Rule::Sqli::MysqlSqlScanner.new,
|
108
|
+
Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_PG =>
|
109
|
+
Contrast::Agent::Protect::Rule::Sqli::PostgresSqlScanner.new,
|
110
|
+
Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_SQLITE =>
|
111
|
+
Contrast::Agent::Protect::Rule::Sqli::SqliteSqlScanner.new,
|
112
|
+
Contrast::Agent::Protect::Policy::AppliesNoSqliRule::DATABASE_NOSQL =>
|
113
|
+
Contrast::Agent::Protect::Rule::NoSqli::MongoNoSqlScanner.new
|
114
|
+
}.cs__freeze
|
115
|
+
|
116
|
+
@default_scanner ||= Contrast::Agent::Protect::Rule::Sqli::DefaultSqlScanner.new
|
117
|
+
@scanners[database.to_s] || @default_scanner
|
118
|
+
end
|
119
|
+
|
120
|
+
def record_match idx, length, boundary, last_boundary, kwargs
|
121
|
+
kwargs[:start_idx] = idx
|
122
|
+
kwargs[:end_idx] = idx + length
|
123
|
+
kwargs[:boundary_overrun_idx] = boundary
|
124
|
+
kwargs[:input_boundary_idx] = last_boundary
|
125
|
+
end
|
126
|
+
|
127
|
+
def append_match context, input_analysis_result, result, query_string, **kwargs
|
128
|
+
input_analysis_result.attack_count = input_analysis_result.attack_count + 1
|
129
|
+
update_successful_attack_response(context, input_analysis_result, result, query_string)
|
130
|
+
append_sample(context, input_analysis_result, result, query_string, **kwargs)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
|
4
4
|
require 'contrast/agent/protect/rule/base_service'
|
5
5
|
require 'contrast/agent/protect/policy/applies_sqli_rule'
|
6
|
+
require 'contrast/agent/protect/rule/sql_sample_builder'
|
6
7
|
|
7
8
|
module Contrast
|
8
9
|
module Agent
|
@@ -10,6 +11,12 @@ module Contrast
|
|
10
11
|
module Rule
|
11
12
|
# The Ruby implementation of the Protect SQL Injection rule.
|
12
13
|
class Sqli < Contrast::Agent::Protect::Rule::BaseService
|
14
|
+
# Generate a sample for the SQLI injection detection rule, allowing for reporting to and rendering
|
15
|
+
# by TeamServer
|
16
|
+
include SqlSampleBuilder::SqliSample
|
17
|
+
# Defining build_attack_with_match method
|
18
|
+
include SqlSampleBuilder::AttackBuilder
|
19
|
+
|
13
20
|
NAME = 'sql-injection'
|
14
21
|
BLOCK_MESSAGE = 'SQLi rule triggered. Response blocked.'
|
15
22
|
|
@@ -31,76 +38,6 @@ module Contrast
|
|
31
38
|
|
32
39
|
raise Contrast::SecurityException.new(self, BLOCK_MESSAGE) if blocked?
|
33
40
|
end
|
34
|
-
|
35
|
-
def build_attack_with_match context, input_analysis_result, result, query_string, **kwargs
|
36
|
-
attack_string = input_analysis_result.value
|
37
|
-
regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE)
|
38
|
-
|
39
|
-
return unless query_string.match?(regexp)
|
40
|
-
|
41
|
-
database = kwargs[:database]
|
42
|
-
scanner = select_scanner(database)
|
43
|
-
|
44
|
-
ss = StringScanner.new(query_string)
|
45
|
-
length = attack_string.length
|
46
|
-
while ss.scan_until(regexp)
|
47
|
-
# the pos of StringScanner is at the end of the regexp (input string),
|
48
|
-
# we need the beginning
|
49
|
-
idx = ss.pos - attack_string.length
|
50
|
-
last_boundary, boundary = scanner.crosses_boundary(query_string, idx, input_analysis_result.value)
|
51
|
-
next unless last_boundary && boundary
|
52
|
-
|
53
|
-
result ||= build_attack_result(context)
|
54
|
-
record_match(idx, length, boundary, last_boundary, kwargs)
|
55
|
-
append_match(context, input_analysis_result, result, query_string, **kwargs)
|
56
|
-
end
|
57
|
-
|
58
|
-
result
|
59
|
-
end
|
60
|
-
|
61
|
-
protected
|
62
|
-
|
63
|
-
def build_sample context, input_analysis_result, candidate_string, **kwargs
|
64
|
-
input = input_analysis_result.value
|
65
|
-
|
66
|
-
sample = build_base_sample(context, input_analysis_result)
|
67
|
-
sample.sqli = Contrast::Api::Dtm::SqlInjectionDetails.new
|
68
|
-
sample.sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
|
69
|
-
sample.sqli.start_idx = sample.sqli.query.index(input).to_i
|
70
|
-
sample.sqli.end_idx = sample.sqli.start_idx + input.length
|
71
|
-
sample.sqli.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i
|
72
|
-
sample.sqli.input_boundary_idx = kwargs[:input_boundary_idx].to_i
|
73
|
-
sample
|
74
|
-
end
|
75
|
-
|
76
|
-
private
|
77
|
-
|
78
|
-
def record_match idx, length, boundary, last_boundary, kwargs
|
79
|
-
kwargs[:start_idx] = idx
|
80
|
-
kwargs[:end_idx] = idx + length
|
81
|
-
kwargs[:boundary_overrun_idx] = boundary
|
82
|
-
kwargs[:input_boundary_idx] = last_boundary
|
83
|
-
end
|
84
|
-
|
85
|
-
def append_match context, input_analysis_result, result, query_string, **kwargs
|
86
|
-
input_analysis_result.attack_count = input_analysis_result.attack_count + 1
|
87
|
-
update_successful_attack_response(context, input_analysis_result, result, query_string)
|
88
|
-
append_sample(context, input_analysis_result, result, query_string, **kwargs)
|
89
|
-
end
|
90
|
-
|
91
|
-
def select_scanner database
|
92
|
-
@sql_scanners ||= {
|
93
|
-
Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_MYSQL =>
|
94
|
-
Contrast::Agent::Protect::Rule::Sqli::MysqlSqlScanner.new,
|
95
|
-
Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_PG =>
|
96
|
-
Contrast::Agent::Protect::Rule::Sqli::PostgresSqlScanner.new,
|
97
|
-
Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_SQLITE =>
|
98
|
-
Contrast::Agent::Protect::Rule::Sqli::SqliteSqlScanner.new
|
99
|
-
}.cs__freeze
|
100
|
-
|
101
|
-
@default_sql_scanner ||= Contrast::Agent::Protect::Rule::Sqli::DefaultSqlScanner.new
|
102
|
-
@sql_scanners[database.to_s] || @default_sql_scanner
|
103
|
-
end
|
104
41
|
end
|
105
42
|
end
|
106
43
|
end
|