contrast-agent 4.9.1 → 4.10.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|