contrast-agent 6.6.5 → 6.7.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/.gitignore +3 -0
- data/.gitmodules +0 -3
- data/ext/cs__scope/cs__scope.c +1 -1
- data/lib/contrast/agent/assess/contrast_event.rb +2 -24
- data/lib/contrast/agent/assess/events/source_event.rb +7 -61
- data/lib/contrast/agent/assess/finalizers/hash.rb +11 -0
- data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +0 -55
- data/lib/contrast/agent/assess/policy/policy_node.rb +3 -3
- data/lib/contrast/agent/assess/policy/policy_node_utils.rb +0 -1
- data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
- data/lib/contrast/agent/assess/policy/source_method.rb +24 -1
- data/lib/contrast/agent/assess/policy/trigger/reflected_xss.rb +7 -5
- data/lib/contrast/agent/assess/policy/trigger/xpath.rb +6 -1
- data/lib/contrast/agent/assess/policy/trigger_method.rb +36 -132
- data/lib/contrast/agent/assess/policy/trigger_node.rb +3 -3
- data/lib/contrast/agent/assess/property/evented.rb +2 -12
- data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +42 -84
- data/lib/contrast/agent/assess/rule/response/base_rule.rb +11 -27
- data/lib/contrast/agent/assess/rule/response/body_rule.rb +1 -3
- data/lib/contrast/agent/assess/rule/response/cache_control_header_rule.rb +77 -62
- data/lib/contrast/agent/assess/rule/response/csp_header_insecure_rule.rb +1 -1
- data/lib/contrast/agent/assess/rule/response/framework/rails_support.rb +6 -1
- data/lib/contrast/agent/assess/rule/response/header_rule.rb +5 -5
- data/lib/contrast/agent/assess/rule/response/hsts_header_rule.rb +1 -1
- data/lib/contrast/agent/assess/rule/response/x_xss_protection_header_rule.rb +1 -1
- data/lib/contrast/agent/assess/tracker.rb +1 -7
- data/lib/contrast/agent/excluder.rb +206 -0
- data/lib/contrast/agent/exclusion_matcher.rb +6 -0
- data/lib/contrast/agent/inventory/database_config.rb +6 -10
- data/lib/contrast/agent/protect/policy/applies_command_injection_rule.rb +4 -0
- data/lib/contrast/agent/protect/policy/applies_sqli_rule.rb +1 -0
- data/lib/contrast/agent/protect/rule/base.rb +49 -5
- data/lib/contrast/agent/protect/rule/base_service.rb +1 -0
- data/lib/contrast/agent/protect/rule/cmd_injection.rb +18 -105
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +129 -0
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +169 -0
- data/lib/contrast/agent/protect/rule/deserialization.rb +2 -1
- data/lib/contrast/agent/protect/rule/sqli/sqli_base_rule.rb +51 -0
- data/lib/contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions.rb +67 -0
- data/lib/contrast/agent/protect/rule/sqli.rb +6 -31
- data/lib/contrast/agent/protect/rule/xxe.rb +2 -0
- data/lib/contrast/agent/protect/rule.rb +3 -1
- data/lib/contrast/agent/reporting/attack_result/rasp_rule_sample.rb +6 -0
- data/lib/contrast/agent/reporting/details/sqli_dangerous_functions.rb +22 -0
- data/lib/contrast/agent/reporting/reporter.rb +1 -2
- data/lib/contrast/agent/reporting/reporting_events/agent_startup.rb +2 -2
- data/lib/contrast/agent/reporting/reporting_events/application_activity.rb +1 -4
- data/lib/contrast/agent/reporting/reporting_events/application_startup.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_events/architecture_component.rb +0 -23
- data/lib/contrast/agent/reporting/reporting_events/finding.rb +19 -49
- data/lib/contrast/agent/reporting/reporting_events/finding_event.rb +12 -9
- data/lib/contrast/agent/reporting/reporting_events/finding_event_signature.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_events/finding_event_source.rb +23 -21
- data/lib/contrast/agent/reporting/reporting_events/finding_event_stack.rb +5 -18
- data/lib/contrast/agent/reporting/reporting_events/finding_event_taint_range.rb +1 -0
- data/lib/contrast/{api/decorators/trace_taint_range_tags.rb → agent/reporting/reporting_events/finding_event_taint_range_tags.rb} +7 -6
- data/lib/contrast/agent/reporting/reporting_events/finding_request.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_events/library_usage_observation.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_events/observed_route.rb +2 -2
- data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +10 -14
- data/lib/contrast/agent/reporting/reporting_events/reporting_event.rb +11 -0
- data/lib/contrast/agent/reporting/reporting_events/route_coverage.rb +3 -1
- data/lib/contrast/agent/reporting/reporting_events/route_discovery.rb +11 -23
- data/lib/contrast/agent/reporting/reporting_events/route_discovery_observation.rb +8 -26
- data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_utilities/build_preflight.rb +4 -7
- data/lib/contrast/agent/reporting/reporting_utilities/headers.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +3 -3
- data/lib/contrast/agent/request.rb +2 -2
- data/lib/contrast/agent/request_context.rb +8 -20
- data/lib/contrast/agent/request_context_extend.rb +15 -36
- data/lib/contrast/agent/request_handler.rb +0 -8
- data/lib/contrast/agent/response.rb +0 -18
- data/lib/contrast/agent/telemetry/events/event.rb +1 -1
- data/lib/contrast/agent/telemetry/events/metric_event.rb +1 -1
- data/lib/contrast/agent/telemetry/events/startup_metrics_event.rb +3 -3
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/api/communication/messaging_queue.rb +2 -3
- data/lib/contrast/api/communication/socket_client.rb +4 -4
- data/lib/contrast/api/communication/speedracer.rb +4 -8
- data/lib/contrast/api/decorators/agent_startup.rb +5 -6
- data/lib/contrast/api/decorators/application_settings.rb +2 -1
- data/lib/contrast/api/decorators/application_startup.rb +6 -6
- data/lib/contrast/api/decorators/message.rb +0 -4
- data/lib/contrast/api/decorators/rasp_rule_sample.rb +0 -6
- data/lib/contrast/api/decorators.rb +0 -6
- data/lib/contrast/api/dtm.pb.rb +0 -489
- data/lib/contrast/components/agent.rb +16 -12
- data/lib/contrast/components/api.rb +10 -10
- data/lib/contrast/components/app_context.rb +3 -3
- data/lib/contrast/components/app_context_extend.rb +1 -1
- data/lib/contrast/components/assess.rb +92 -38
- data/lib/contrast/components/assess_rules.rb +36 -0
- data/lib/contrast/components/config.rb +54 -12
- data/lib/contrast/components/contrast_service.rb +8 -8
- data/lib/contrast/components/heap_dump.rb +1 -1
- data/lib/contrast/components/protect.rb +5 -5
- data/lib/contrast/components/ruby_component.rb +81 -0
- data/lib/contrast/components/sampling.rb +1 -1
- data/lib/contrast/components/security_logger.rb +23 -0
- data/lib/contrast/components/service.rb +55 -0
- data/lib/contrast/components/settings.rb +12 -4
- data/lib/contrast/config/base_configuration.rb +1 -1
- data/lib/contrast/config/protect_rules_configuration.rb +17 -3
- data/lib/contrast/config/server_configuration.rb +1 -1
- data/lib/contrast/config.rb +0 -6
- data/lib/contrast/configuration.rb +81 -17
- data/lib/contrast/extension/assess/exec_trigger.rb +3 -1
- data/lib/contrast/extension/assess/marshal.rb +3 -2
- data/lib/contrast/extension/assess/string.rb +0 -1
- data/lib/contrast/extension/extension.rb +1 -1
- data/lib/contrast/framework/base_support.rb +0 -5
- data/lib/contrast/framework/grape/support.rb +1 -23
- data/lib/contrast/framework/manager.rb +0 -10
- data/lib/contrast/framework/rails/support.rb +5 -58
- data/lib/contrast/framework/sinatra/support.rb +2 -21
- data/lib/contrast/logger/cef_log.rb +21 -3
- data/lib/contrast/logger/log.rb +1 -11
- data/lib/contrast/tasks/config.rb +4 -2
- data/lib/contrast/utils/assess/event_limit_utils.rb +5 -8
- data/lib/contrast/utils/assess/trigger_method_utils.rb +10 -18
- data/lib/contrast/utils/findings.rb +6 -5
- data/lib/contrast/utils/hash_digest.rb +9 -24
- data/lib/contrast/utils/hash_digest_extend.rb +6 -6
- data/lib/contrast/utils/invalid_configuration_util.rb +21 -58
- data/lib/contrast/utils/log_utils.rb +32 -8
- data/lib/contrast/utils/net_http_base.rb +2 -2
- data/lib/contrast/utils/patching/policy/patch_utils.rb +3 -2
- data/lib/contrast/utils/stack_trace_utils.rb +0 -25
- data/lib/contrast/utils/string_utils.rb +9 -0
- data/lib/contrast/utils/telemetry_client.rb +13 -7
- data/lib/contrast.rb +5 -10
- metadata +22 -28
- data/lib/contrast/agent/reporting/reporting_events/trace_event_source.rb +0 -30
- data/lib/contrast/agent/reporting/reporting_utilities/dtm_message.rb +0 -36
- data/lib/contrast/api/decorators/activity.rb +0 -33
- data/lib/contrast/api/decorators/architecture_component.rb +0 -36
- data/lib/contrast/api/decorators/finding.rb +0 -29
- data/lib/contrast/api/decorators/route_coverage.rb +0 -91
- data/lib/contrast/api/decorators/trace_event.rb +0 -120
- data/lib/contrast/api/decorators/trace_event_object.rb +0 -63
- data/lib/contrast/api/decorators/trace_event_signature.rb +0 -69
- data/lib/contrast/api/decorators/trace_taint_range.rb +0 -52
- data/lib/contrast/config/assess_configuration.rb +0 -93
- data/lib/contrast/config/assess_rules_configuration.rb +0 -32
- data/lib/contrast/config/root_configuration.rb +0 -90
- data/lib/contrast/config/ruby_configuration.rb +0 -81
- data/lib/contrast/config/service_configuration.rb +0 -49
- data/lib/contrast/utils/preflight_util.rb +0 -13
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require 'contrast/components/logger'
|
|
5
5
|
require 'contrast/components/scope'
|
|
6
|
+
require 'contrast/utils/object_share'
|
|
6
7
|
require 'contrast/api/decorators/response_type'
|
|
7
8
|
|
|
8
9
|
module Contrast
|
|
@@ -34,6 +35,11 @@ module Contrast
|
|
|
34
35
|
Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED,
|
|
35
36
|
Contrast::Api::Dtm::AttackResult::ResponseType::MONITORED
|
|
36
37
|
]).cs__freeze
|
|
38
|
+
SUSPICIOUS_REPORTING_RULES = %w[
|
|
39
|
+
unsafe-file-upload
|
|
40
|
+
reflected-xss
|
|
41
|
+
sql-injection-semantic-dangerous-functions
|
|
42
|
+
].cs__freeze
|
|
37
43
|
|
|
38
44
|
attr_reader :mode
|
|
39
45
|
|
|
@@ -47,6 +53,12 @@ module Contrast
|
|
|
47
53
|
cs__class.cs__name
|
|
48
54
|
end
|
|
49
55
|
|
|
56
|
+
# Should return list of all sub_rules.
|
|
57
|
+
# Extend for each main rule any sub-rules.
|
|
58
|
+
def sub_rules
|
|
59
|
+
Contrast::Utils::ObjectShare::EMPTY_ARRAY
|
|
60
|
+
end
|
|
61
|
+
|
|
50
62
|
def enabled?
|
|
51
63
|
# 1. it is not enabled because protect is not enabled
|
|
52
64
|
return false unless ::Contrast::AGENT.enabled?
|
|
@@ -215,6 +227,12 @@ module Contrast
|
|
|
215
227
|
for_rule.any? { |ex| ex.match_code?(stack) }
|
|
216
228
|
end
|
|
217
229
|
|
|
230
|
+
# @param context [Contrast::Agent::RequestContext] the context of the
|
|
231
|
+
# request in which this input is evaluated.
|
|
232
|
+
def protect_excluded_by_url? context
|
|
233
|
+
Contrast::SETTINGS.excluder.protect_excluded_by_url?(context.request)
|
|
234
|
+
end
|
|
235
|
+
|
|
218
236
|
# By default, rules do not have to find attackers as they do not have
|
|
219
237
|
# Input Analysis. Any attack for the standard rule will be evaluated
|
|
220
238
|
# at execution time. As such, those rules are expected to implement
|
|
@@ -234,7 +252,12 @@ module Contrast
|
|
|
234
252
|
def update_successful_attack_response context, ia_result, result, attack_string = nil
|
|
235
253
|
case mode
|
|
236
254
|
when Contrast::Api::Settings::ProtectionRule::Mode::MONITOR
|
|
237
|
-
result
|
|
255
|
+
# We are checking the result as the ia_result would not contain the sub-rules.
|
|
256
|
+
result.response = if SUSPICIOUS_REPORTING_RULES.include?(result&.rule_id)
|
|
257
|
+
Contrast::Agent::Reporting::ResponseType::SUSPICIOUS
|
|
258
|
+
else
|
|
259
|
+
Contrast::Agent::Reporting::ResponseType::MONITORED
|
|
260
|
+
end
|
|
238
261
|
when Contrast::Api::Settings::ProtectionRule::Mode::BLOCK
|
|
239
262
|
result.response = Contrast::Agent::Reporting::ResponseType::BLOCKED
|
|
240
263
|
end
|
|
@@ -279,6 +302,9 @@ module Contrast
|
|
|
279
302
|
result
|
|
280
303
|
end
|
|
281
304
|
|
|
305
|
+
# @param sample [Contrast::Agent::Reporting::RaspRuleSample]
|
|
306
|
+
# @param result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule, if one exists,
|
|
307
|
+
# in the case of multiple inputs being found to violate the protection criteria
|
|
282
308
|
def append_stack sample, result
|
|
283
309
|
return unless sample
|
|
284
310
|
return unless STACK_COLLECTION_RESULTS.include?(result&.response)
|
|
@@ -289,6 +315,16 @@ module Contrast
|
|
|
289
315
|
sample.stack_trace_elements += stack
|
|
290
316
|
end
|
|
291
317
|
|
|
318
|
+
# @param context [Contrast::Agent::RequestContext] the context of the request in which this input is
|
|
319
|
+
# evaluated.
|
|
320
|
+
# @param ia_result [Contrast::Api::Settings::InputAnalysisResult] the analysis of the input that was
|
|
321
|
+
# determined to be an attack
|
|
322
|
+
# @param result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule, if one exists,
|
|
323
|
+
# in the case of multiple inputs being found to violate the protection criteria
|
|
324
|
+
# @param candidate_string [String] the value of the input which may be an attack
|
|
325
|
+
# @param kwargs [Hash] key - value pairs of context individual rules
|
|
326
|
+
# need to build out details to send to the Service to tell the
|
|
327
|
+
# story of the attack
|
|
292
328
|
def append_sample context, ia_result, result, candidate_string, **kwargs
|
|
293
329
|
return unless result
|
|
294
330
|
|
|
@@ -302,10 +338,21 @@ module Contrast
|
|
|
302
338
|
|
|
303
339
|
# Override if rule can make use of the candidate string or kwargs to
|
|
304
340
|
# build rasp rule sample.
|
|
341
|
+
#
|
|
342
|
+
# @param context [Contrast::Agent::RequestContext]
|
|
343
|
+
# @param ia_result [Contrast::Api::Settings::InputAnalysisResult] the analysis of the input that was
|
|
344
|
+
# determined to be an attack
|
|
345
|
+
# @param _candidate_string [String] potential attack value/ input containing attack value
|
|
346
|
+
# @param _kwargs [Hash]
|
|
347
|
+
# @return [Contrast::Agent::Reporting::RaspRuleSample]
|
|
305
348
|
def build_sample context, ia_result, _candidate_string, **_kwargs
|
|
306
349
|
build_base_sample(context, ia_result)
|
|
307
350
|
end
|
|
308
351
|
|
|
352
|
+
# @param context [Contrast::Agent::RequestContext]
|
|
353
|
+
# @param ia_result [Contrast::Api::Settings::InputAnalysisResult] the analysis of the input that was
|
|
354
|
+
# determined to be an attack
|
|
355
|
+
# @return [Contrast::Agent::Reporting::RaspRuleSample]
|
|
309
356
|
def build_base_sample context, ia_result
|
|
310
357
|
Contrast::Agent::Reporting::RaspRuleSample.build(context, ia_result)
|
|
311
358
|
end
|
|
@@ -349,10 +396,7 @@ module Contrast
|
|
|
349
396
|
# @param ia_result
|
|
350
397
|
# @return [Boolean]
|
|
351
398
|
def suspicious_rule? ia_result
|
|
352
|
-
|
|
353
|
-
Contrast::Agent::Protect::Rule::UnsafeFileUpload::NAME,
|
|
354
|
-
Contrast::Agent::Protect::Rule::Xss::NAME
|
|
355
|
-
].include?(ia_result&.rule_id)
|
|
399
|
+
SUSPICIOUS_REPORTING_RULES.include?(ia_result&.rule_id)
|
|
356
400
|
end
|
|
357
401
|
|
|
358
402
|
# Handles the Response type for different Protect rules. Some rules need to report SUSPICIOUS over PROBED in
|
|
@@ -6,41 +6,36 @@ require 'contrast/utils/stack_trace_utils'
|
|
|
6
6
|
require 'contrast/utils/object_share'
|
|
7
7
|
require 'contrast/components/logger'
|
|
8
8
|
require 'contrast/agent/reporting/input_analysis/input_type'
|
|
9
|
-
require 'contrast/agent/
|
|
9
|
+
require 'contrast/agent/protect/rule/cmdi/cmdi_base_rule'
|
|
10
|
+
require 'contrast/agent/protect/rule/cmdi/cmdi_backdoors'
|
|
10
11
|
|
|
11
12
|
module Contrast
|
|
12
13
|
module Agent
|
|
13
14
|
module Protect
|
|
14
15
|
module Rule
|
|
15
16
|
# The Ruby implementation of the Protect Command Injection rule.
|
|
16
|
-
class CmdInjection < Contrast::Agent::Protect::Rule::
|
|
17
|
+
class CmdInjection < Contrast::Agent::Protect::Rule::CmdiBaseRule
|
|
17
18
|
include Contrast::Components::Logger::InstanceMethods
|
|
18
19
|
include Contrast::Agent::Reporting::InputType
|
|
19
|
-
|
|
20
20
|
NAME = 'cmd-injection'
|
|
21
|
-
|
|
22
|
-
APPLICABLE_USER_INPUTS = [
|
|
23
|
-
BODY, COOKIE_VALUE, HEADER, PARAMETER_NAME,
|
|
24
|
-
PARAMETER_VALUE, JSON_VALUE, MULTIPART_VALUE,
|
|
25
|
-
MULTIPART_FIELD_NAME, XML_VALUE, DWR_VALUE
|
|
26
|
-
].cs__freeze
|
|
27
|
-
|
|
28
|
-
class << self
|
|
29
|
-
# @param attack_sample [Contrast::Api::Dtm::RaspRuleSample]
|
|
30
|
-
# @return [Hash] the details for this specific rule
|
|
31
|
-
def extract_details attack_sample
|
|
32
|
-
{
|
|
33
|
-
command: attack_sample.cmdi.command,
|
|
34
|
-
startIndex: attack_sample.cmdi.start_idx,
|
|
35
|
-
endIndex: attack_sample.cmdi.end_idx
|
|
36
|
-
}
|
|
37
|
-
end
|
|
38
|
-
end
|
|
21
|
+
SUB_RULES = [Contrast::Agent::Protect::Rule::CmdiBackdoors.new].cs__freeze
|
|
39
22
|
|
|
40
23
|
def rule_name
|
|
41
24
|
NAME
|
|
42
25
|
end
|
|
43
26
|
|
|
27
|
+
def sub_rules
|
|
28
|
+
SUB_RULES
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# CMDI infilter:
|
|
32
|
+
#
|
|
33
|
+
# @param context [Contrast::Agent::RequestContext] current request context
|
|
34
|
+
# @param classname [String] Name of the class
|
|
35
|
+
# @param method [String] name of the method triggering the rule
|
|
36
|
+
# @param command [String] potential dangerous command executed.
|
|
37
|
+
# @raise [Contrast::SecurityException] if the rule mode is set
|
|
38
|
+
# to BLOCK and valid cdmi is detected.
|
|
44
39
|
def infilter context, classname, method, command
|
|
45
40
|
return unless infilter?(context)
|
|
46
41
|
|
|
@@ -63,90 +58,8 @@ module Contrast
|
|
|
63
58
|
|
|
64
59
|
return unless blocked?
|
|
65
60
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"Call to #{ classname }.#{ method } blocked."))
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def build_attack_with_match context, input_analysis_result, result, candidate_string, **kwargs
|
|
72
|
-
if mode == Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION ||
|
|
73
|
-
mode == Contrast::Api::Settings::ProtectionRule::Mode::PERMIT
|
|
74
|
-
|
|
75
|
-
return result
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
result ||= build_attack_result(context)
|
|
79
|
-
update_successful_attack_response(context, input_analysis_result, result, candidate_string)
|
|
80
|
-
append_sample(context, input_analysis_result, result, candidate_string, **kwargs)
|
|
81
|
-
result
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
protected
|
|
85
|
-
|
|
86
|
-
# Because results are not necessarily on the context across
|
|
87
|
-
# processes; extract early and pass into the method
|
|
88
|
-
def find_attacker_with_results context, potential_attack_string, ia_results, **kwargs
|
|
89
|
-
logger.trace('Checking vectors for attacks', rule: rule_name, input: potential_attack_string)
|
|
90
|
-
result = super(context, potential_attack_string, ia_results, **kwargs)
|
|
91
|
-
if result.nil? && potential_attack_string
|
|
92
|
-
result = find_probable_attacker(context, potential_attack_string, ia_results, **kwargs)
|
|
93
|
-
end
|
|
94
|
-
result
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Build a subclass of the RaspRuleSample using the query string and the
|
|
98
|
-
# evaluation
|
|
99
|
-
def build_sample context, input_analysis_result, candidate_string, **_kwargs
|
|
100
|
-
sample = build_base_sample(context, input_analysis_result)
|
|
101
|
-
sample.details = Contrast::Agent::Reporting::Details::CmdInjectionDetails.new
|
|
102
|
-
|
|
103
|
-
command = candidate_string || input_analysis_result.value
|
|
104
|
-
command = Contrast::Utils::StringUtils.protobuf_safe_string(command)
|
|
105
|
-
sample.details.cmd = command
|
|
106
|
-
sample.details.end_idx = command.length
|
|
107
|
-
|
|
108
|
-
# This is a special case where the user input is UNKNOWN_USER_INPUT but
|
|
109
|
-
# we want to send the attack value
|
|
110
|
-
if input_analysis_result.nil?
|
|
111
|
-
ui = Contrast::Agent::Reporting::UserInput.new
|
|
112
|
-
ui.input_type = :UNKNOWN
|
|
113
|
-
ui.value = command
|
|
114
|
-
sample.user_input = ui
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
sample
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
private
|
|
121
|
-
|
|
122
|
-
def report_command_execution context, command, **kwargs
|
|
123
|
-
return unless report_any_command_execution?
|
|
124
|
-
return if protect_excluded_by_code?
|
|
125
|
-
|
|
126
|
-
build_attack_with_match(context, nil, nil, command, **kwargs)
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def find_probable_attacker context, potential_attack_string, ia_results, **kwargs
|
|
130
|
-
return unless chained_command?(potential_attack_string)
|
|
131
|
-
|
|
132
|
-
likely_attacker = ia_results.find { |input_analysis_result| chained_command?(input_analysis_result.value) }
|
|
133
|
-
return unless likely_attacker
|
|
134
|
-
|
|
135
|
-
build_attack_with_match(context, likely_attacker, nil, potential_attack_string, **kwargs)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def chained_command? command
|
|
139
|
-
CHAINED_COMMAND_CHARS.match(command)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Part of the Hardening for Command Injection detection is the
|
|
143
|
-
# ability to detect and prevent any command execution from within the
|
|
144
|
-
# application. This check determines if that hardening has been
|
|
145
|
-
# enabled.
|
|
146
|
-
# @return [Boolean] if the agent should report all command
|
|
147
|
-
# executions.
|
|
148
|
-
def report_any_command_execution?
|
|
149
|
-
::Contrast::PROTECT.report_any_command_execution?
|
|
61
|
+
# Raise cmdi error
|
|
62
|
+
raise_error(classname, method)
|
|
150
63
|
end
|
|
151
64
|
end
|
|
152
65
|
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Copyright (c) 2022 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_service'
|
|
5
|
+
require 'contrast/agent/request_context'
|
|
6
|
+
require 'contrast/utils/object_share'
|
|
7
|
+
require 'contrast/agent/protect/rule/cmdi/cmdi_base_rule'
|
|
8
|
+
require 'contrast/agent/protect/rule/cmd_injection'
|
|
9
|
+
|
|
10
|
+
module Contrast
|
|
11
|
+
module Agent
|
|
12
|
+
module Protect
|
|
13
|
+
module Rule
|
|
14
|
+
# The Ruby implementation of the Protect Command Injection Command
|
|
15
|
+
# Backdoors sub-rule.
|
|
16
|
+
class CmdiBackdoors < Contrast::Agent::Protect::Rule::CmdiBaseRule
|
|
17
|
+
NAME = 'cmd-injection-command-backdoors'
|
|
18
|
+
MATCHES = %w[/bin/bash-c /bin/sh-c sh-c bash-c].cs__freeze
|
|
19
|
+
|
|
20
|
+
def rule_name
|
|
21
|
+
NAME
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# CMDI Backdoors infilter:
|
|
25
|
+
# This rule does not have input classification.
|
|
26
|
+
# If a value matches the CMDI applicable input types and it's length is > 2
|
|
27
|
+
# we can check if it's used as command backdoors.
|
|
28
|
+
#
|
|
29
|
+
# @param context [Contrast::Agent::RequestContext] current request contest
|
|
30
|
+
# @param classname [String] Name of the class
|
|
31
|
+
# @param method [String] name of the method triggering the rule
|
|
32
|
+
# @param command [String] potential dangerous command executed.
|
|
33
|
+
# @raise [Contrast::SecurityException] if the rule mode ise set
|
|
34
|
+
# to BLOCK and valid cdmi is detected.
|
|
35
|
+
def infilter context, classname, method, command
|
|
36
|
+
return unless (ia_results = gather_ia_results(context))
|
|
37
|
+
return unless backdoors_match?(command)
|
|
38
|
+
return unless (likely_attacker = match_applicable_input_type(ia_results, command))
|
|
39
|
+
return if protect_excluded_by_code?
|
|
40
|
+
return unless (result = build_attack_with_match(context, likely_attacker, nil, command,
|
|
41
|
+
**{ classname: classname, method: method }))
|
|
42
|
+
|
|
43
|
+
append_to_activity(context, result)
|
|
44
|
+
cef_logging(result, :successful_attack)
|
|
45
|
+
return unless blocked?
|
|
46
|
+
|
|
47
|
+
raise_error(classname, method)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param context [Contrast::Agent::RequestContext]
|
|
51
|
+
def infilter? context
|
|
52
|
+
return false unless enabled?
|
|
53
|
+
# This rule does not have input tracing stage so we need to check the results of
|
|
54
|
+
# the main rule instead.
|
|
55
|
+
return false unless context&.speedracer_input_analysis&.results&.any? do |result|
|
|
56
|
+
result.rule_id == Contrast::Agent::Protect::Rule::CmdInjection::NAME
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return false if protect_excluded_by_code?
|
|
60
|
+
|
|
61
|
+
true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
protected
|
|
65
|
+
|
|
66
|
+
# Used to customize the raised error message.
|
|
67
|
+
#
|
|
68
|
+
# @param classname [String] Name of the class
|
|
69
|
+
# @param method [String] name of the method triggering the rule
|
|
70
|
+
# @raise [Contrast::SecurityException]
|
|
71
|
+
def raise_error classname, method
|
|
72
|
+
raise(Contrast::SecurityException.new(self,
|
|
73
|
+
'Command Injection Command Backdoor rule triggered. '\
|
|
74
|
+
"Call to #{ classname }.#{ method } blocked."))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Check to see if value is backdoor match
|
|
80
|
+
#
|
|
81
|
+
# @param potential_attack_string [String]
|
|
82
|
+
def backdoors_match? potential_attack_string
|
|
83
|
+
return false unless potential_attack_string && potential_attack_string.length > 2
|
|
84
|
+
return false unless matches_shell_parameter?(potential_attack_string)
|
|
85
|
+
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Checks to see if input is used as parameter for a shell cmd.
|
|
90
|
+
#
|
|
91
|
+
# @param cmd [String]
|
|
92
|
+
# @return [Boolean]
|
|
93
|
+
def matches_shell_parameter? cmd
|
|
94
|
+
normalize_cmd = cmd.delete(Contrast::Utils::ObjectShare::SPACE)
|
|
95
|
+
MATCHES.each do |match|
|
|
96
|
+
return true if normalize_cmd.include?(match)
|
|
97
|
+
end
|
|
98
|
+
false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Check to see if the user input is one of the applicable types.
|
|
102
|
+
# With Agent_ia if we are here and CMDI is being analyzed then
|
|
103
|
+
# the applicable input type is already checked before input
|
|
104
|
+
# classification. Only thing left is the match check.
|
|
105
|
+
#
|
|
106
|
+
# @param ia_results [Contrast::Api::Settings::InputAnalysisResult] gathered results
|
|
107
|
+
# @param cmd [String] potential attack vector
|
|
108
|
+
# @return [Contrast::Api::Settings::InputAnalysisResult, nil] matched input_type
|
|
109
|
+
def match_applicable_input_type ia_results, cmd
|
|
110
|
+
ia_results.each do |ia_result|
|
|
111
|
+
return ia_result if ia_result.value == cmd
|
|
112
|
+
end
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Backdoors does not have input classification so we check for match within the
|
|
117
|
+
# result for the main rule - CMDI.
|
|
118
|
+
#
|
|
119
|
+
# @param context [Contrast::Agent::RequestContext]
|
|
120
|
+
def gather_ia_results context
|
|
121
|
+
context.speedracer_input_analysis.results.select do |ia_result|
|
|
122
|
+
ia_result.rule_id == Contrast::Agent::Protect::Rule::CmdInjection::NAME
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'contrast/agent/reporting/details/cmd_injection_details'
|
|
5
|
+
require 'contrast/agent/reporting/attack_result/user_input'
|
|
6
|
+
|
|
7
|
+
module Contrast
|
|
8
|
+
module Agent
|
|
9
|
+
module Protect
|
|
10
|
+
module Rule
|
|
11
|
+
# The Ruby implementation of the Protect Command Injection Semantic
|
|
12
|
+
# Dangerous Path sub-rule. This rule should report
|
|
13
|
+
class CmdiBaseRule < Contrast::Agent::Protect::Rule::BaseService
|
|
14
|
+
include Contrast::Components::Logger::InstanceMethods
|
|
15
|
+
include Contrast::Agent::Reporting::InputType
|
|
16
|
+
|
|
17
|
+
CHAINED_COMMAND_CHARS = /[;&|<>]/.cs__freeze
|
|
18
|
+
APPLICABLE_USER_INPUTS = [
|
|
19
|
+
BODY, COOKIE_VALUE, HEADER, PARAMETER_NAME,
|
|
20
|
+
PARAMETER_VALUE, JSON_VALUE, MULTIPART_VALUE,
|
|
21
|
+
MULTIPART_FIELD_NAME, XML_VALUE, DWR_VALUE
|
|
22
|
+
].cs__freeze
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# @param attack_sample [Contrast::Api::Dtm::RaspRuleSample]
|
|
26
|
+
# @return [Hash] the details for this specific rule
|
|
27
|
+
def extract_details attack_sample
|
|
28
|
+
{
|
|
29
|
+
command: attack_sample.cmdi.command,
|
|
30
|
+
startIndex: attack_sample.cmdi.start_idx,
|
|
31
|
+
endIndex: attack_sample.cmdi.end_idx
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# CMDI infilter:
|
|
37
|
+
#
|
|
38
|
+
# @param context [Contrast::Agent::RequestContext] current request contest
|
|
39
|
+
# @param classname [String] Name of the class
|
|
40
|
+
# @param method [String] name of the method triggering the rule
|
|
41
|
+
# @param command [String] potential dangerous command executed.
|
|
42
|
+
# @raise [Contrast::SecurityException] if the rule mode ise set
|
|
43
|
+
# to BLOCK and valid cdmi is detected.
|
|
44
|
+
def infilter context, classname, method, command
|
|
45
|
+
if ::Contrast::APP_CONTEXT.in_new_process?
|
|
46
|
+
logger.trace('Running cmd-injection infilter within new process - creating new context')
|
|
47
|
+
context = Contrast::Agent::RequestContext.new(context.request.rack_request)
|
|
48
|
+
Contrast::Agent::REQUEST_TRACKER.update_current_context(context)
|
|
49
|
+
end
|
|
50
|
+
return unless infilter?(context)
|
|
51
|
+
|
|
52
|
+
result = find_attacker_with_results(context, command, nil, **{ classname: classname, method: method })
|
|
53
|
+
result ||= report_command_execution(context, command, **{ classname: classname, method: method })
|
|
54
|
+
return unless result
|
|
55
|
+
|
|
56
|
+
append_to_activity(context, result)
|
|
57
|
+
cef_logging(result, :successful_attack)
|
|
58
|
+
return unless blocked?
|
|
59
|
+
|
|
60
|
+
raise_error(classname, method)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_attack_with_match(context,
|
|
64
|
+
input_analysis_result,
|
|
65
|
+
result,
|
|
66
|
+
candidate_string,
|
|
67
|
+
**kwargs)
|
|
68
|
+
if mode == Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION ||
|
|
69
|
+
mode == Contrast::Api::Settings::ProtectionRule::Mode::PERMIT
|
|
70
|
+
|
|
71
|
+
return result
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
result ||= build_attack_result(context)
|
|
75
|
+
update_successful_attack_response(context, input_analysis_result, result, candidate_string)
|
|
76
|
+
append_sample(context, input_analysis_result, result, candidate_string, **kwargs)
|
|
77
|
+
result
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
protected
|
|
81
|
+
|
|
82
|
+
# Used to customize the raised error message.
|
|
83
|
+
#
|
|
84
|
+
# @param classname [String] Name of the class
|
|
85
|
+
# @param method [String] name of the method triggering the rule
|
|
86
|
+
# @raise [Contrast::SecurityException]
|
|
87
|
+
def raise_error classname, method
|
|
88
|
+
raise(Contrast::SecurityException.new(self,
|
|
89
|
+
'Command Injection Rule triggered. '\
|
|
90
|
+
"Call to #{ classname }.#{ method } blocked."))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Allows for the InputAnalysis from service to be extracted early.
|
|
94
|
+
# Because results are not necessarily on the context across
|
|
95
|
+
# processes; extract early and pass into the method
|
|
96
|
+
#
|
|
97
|
+
# @param context [Contrast::Agent::RequestContext]
|
|
98
|
+
# @param potential_attack_string [String, nil]
|
|
99
|
+
# @param ia_results [Array<Contrast::Api::Settings::InputAnalysis>]
|
|
100
|
+
# @param **kwargs
|
|
101
|
+
# @return [Contrast::Api::Dtm::AttackResult, nil]
|
|
102
|
+
def find_attacker_with_results context, potential_attack_string, ia_results, **kwargs
|
|
103
|
+
logger.trace('Checking vectors for attacks', rule: rule_name, input: potential_attack_string)
|
|
104
|
+
result = super(context, potential_attack_string, ia_results, **kwargs) if ia_results
|
|
105
|
+
if result.nil? && potential_attack_string
|
|
106
|
+
result = find_probable_attacker(context, potential_attack_string, ia_results, **kwargs)
|
|
107
|
+
end
|
|
108
|
+
result
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Build a subclass of the RaspRuleSample using the query string and the
|
|
112
|
+
# evaluation
|
|
113
|
+
def build_sample context, input_analysis_result, candidate_string, **_kwargs
|
|
114
|
+
sample = build_base_sample(context, input_analysis_result)
|
|
115
|
+
sample.details = Contrast::Agent::Reporting::Details::CmdInjectionDetails.new
|
|
116
|
+
|
|
117
|
+
command = candidate_string || input_analysis_result.value
|
|
118
|
+
command = Contrast::Utils::StringUtils.protobuf_safe_string(command)
|
|
119
|
+
sample.details.cmd = command
|
|
120
|
+
sample.details.end_idx = command.length
|
|
121
|
+
|
|
122
|
+
# This is a special case where the user input is UNKNOWN_USER_INPUT but
|
|
123
|
+
# we want to send the attack value
|
|
124
|
+
if input_analysis_result.nil?
|
|
125
|
+
ui = Contrast::Agent::Reporting::UserInput.new
|
|
126
|
+
ui.input_type = :UNKNOWN
|
|
127
|
+
ui.value = command
|
|
128
|
+
sample.user_input = ui
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
sample
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def report_command_execution context, command, **kwargs
|
|
137
|
+
return unless report_any_command_execution?
|
|
138
|
+
return if protect_excluded_by_code?
|
|
139
|
+
|
|
140
|
+
build_attack_with_match(context, nil, nil, command, **kwargs)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def find_probable_attacker context, potential_attack_string, ia_results, **kwargs
|
|
144
|
+
return unless chained_command?(potential_attack_string)
|
|
145
|
+
|
|
146
|
+
likely_attacker = ia_results.find { |input_analysis_result| chained_command?(input_analysis_result.value) }
|
|
147
|
+
return unless likely_attacker
|
|
148
|
+
|
|
149
|
+
build_attack_with_match(context, likely_attacker, nil, potential_attack_string, **kwargs)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def chained_command? command
|
|
153
|
+
CHAINED_COMMAND_CHARS.match(command)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Part of the Hardening for Command Injection detection is the
|
|
157
|
+
# ability to detect and prevent any command execution from within the
|
|
158
|
+
# application. This check determines if that hardening has been
|
|
159
|
+
# enabled.
|
|
160
|
+
# @return [Boolean] if the agent should report all command
|
|
161
|
+
# executions.
|
|
162
|
+
def report_any_command_execution?
|
|
163
|
+
::Contrast::PROTECT.report_any_command_execution?
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -69,9 +69,10 @@ module Contrast
|
|
|
69
69
|
# Per the spec, this rule applies regardless of input. Only the mode
|
|
70
70
|
# of the rule and code exclusions apply at this point.
|
|
71
71
|
# @return [Boolean] should the rule apply to this call.
|
|
72
|
-
def infilter?
|
|
72
|
+
def infilter? context
|
|
73
73
|
return false unless enabled?
|
|
74
74
|
return false if protect_excluded_by_code?
|
|
75
|
+
return false if protect_excluded_by_url?(context)
|
|
75
76
|
|
|
76
77
|
true
|
|
77
78
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Contrast
|
|
5
|
+
module Agent
|
|
6
|
+
module Protect
|
|
7
|
+
module Rule
|
|
8
|
+
# This is the Base Rule
|
|
9
|
+
class SqliBaseRule < Contrast::Agent::Protect::Rule::BaseService
|
|
10
|
+
include Contrast::Components::Logger::InstanceMethods
|
|
11
|
+
include Contrast::Agent::Reporting::InputType
|
|
12
|
+
|
|
13
|
+
BLOCK_MESSAGE = 'SQLi rule triggered. Response blocked.'
|
|
14
|
+
|
|
15
|
+
APPLICABLE_USER_INPUTS = [
|
|
16
|
+
BODY, COOKIE_NAME, COOKIE_VALUE, HEADER,
|
|
17
|
+
PARAMETER_NAME, PARAMETER_VALUE, JSON_VALUE,
|
|
18
|
+
MULTIPART_VALUE, MULTIPART_FIELD_NAME,
|
|
19
|
+
XML_VALUE, DWR_VALUE
|
|
20
|
+
].cs__freeze
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# @param attack_sample [Contrast::Api::Dtm::RaspRuleSample]
|
|
24
|
+
# @return [Hash] the details for this specific rule
|
|
25
|
+
def extract_details attack_sample
|
|
26
|
+
{
|
|
27
|
+
start: attack_sample.sqli.start_idx,
|
|
28
|
+
end: attack_sample.sqli.end_idx,
|
|
29
|
+
boundaryOverrunIndex: attack_sample.sqli.boundary_overrun_idx,
|
|
30
|
+
inputBoundaryIndex: attack_sample.sqli.input_boundary_idx,
|
|
31
|
+
query: attack_sample.sqli.query
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def infilter context, database, query_string
|
|
37
|
+
return unless infilter?(context)
|
|
38
|
+
|
|
39
|
+
result = find_attacker(context, query_string, database: database)
|
|
40
|
+
return unless result
|
|
41
|
+
|
|
42
|
+
append_to_activity(context, result)
|
|
43
|
+
|
|
44
|
+
cef_logging(result, :successful_attack)
|
|
45
|
+
raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if blocked?
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|