contrast-agent 6.6.5 → 6.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (150) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.gitmodules +0 -3
  4. data/ext/cs__scope/cs__scope.c +1 -1
  5. data/lib/contrast/agent/assess/contrast_event.rb +2 -24
  6. data/lib/contrast/agent/assess/events/source_event.rb +7 -61
  7. data/lib/contrast/agent/assess/finalizers/hash.rb +11 -0
  8. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +0 -55
  9. data/lib/contrast/agent/assess/policy/policy_node.rb +3 -3
  10. data/lib/contrast/agent/assess/policy/policy_node_utils.rb +0 -1
  11. data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
  12. data/lib/contrast/agent/assess/policy/source_method.rb +24 -1
  13. data/lib/contrast/agent/assess/policy/trigger/reflected_xss.rb +7 -5
  14. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +6 -1
  15. data/lib/contrast/agent/assess/policy/trigger_method.rb +36 -132
  16. data/lib/contrast/agent/assess/policy/trigger_node.rb +3 -3
  17. data/lib/contrast/agent/assess/property/evented.rb +2 -12
  18. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +42 -84
  19. data/lib/contrast/agent/assess/rule/response/base_rule.rb +11 -27
  20. data/lib/contrast/agent/assess/rule/response/body_rule.rb +1 -3
  21. data/lib/contrast/agent/assess/rule/response/cache_control_header_rule.rb +77 -62
  22. data/lib/contrast/agent/assess/rule/response/csp_header_insecure_rule.rb +1 -1
  23. data/lib/contrast/agent/assess/rule/response/framework/rails_support.rb +6 -1
  24. data/lib/contrast/agent/assess/rule/response/header_rule.rb +5 -5
  25. data/lib/contrast/agent/assess/rule/response/hsts_header_rule.rb +1 -1
  26. data/lib/contrast/agent/assess/rule/response/x_xss_protection_header_rule.rb +1 -1
  27. data/lib/contrast/agent/assess/tracker.rb +1 -7
  28. data/lib/contrast/agent/excluder.rb +206 -0
  29. data/lib/contrast/agent/exclusion_matcher.rb +6 -0
  30. data/lib/contrast/agent/inventory/database_config.rb +6 -10
  31. data/lib/contrast/agent/protect/policy/applies_command_injection_rule.rb +4 -0
  32. data/lib/contrast/agent/protect/policy/applies_sqli_rule.rb +1 -0
  33. data/lib/contrast/agent/protect/rule/base.rb +49 -5
  34. data/lib/contrast/agent/protect/rule/base_service.rb +1 -0
  35. data/lib/contrast/agent/protect/rule/cmd_injection.rb +18 -105
  36. data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +129 -0
  37. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +169 -0
  38. data/lib/contrast/agent/protect/rule/deserialization.rb +2 -1
  39. data/lib/contrast/agent/protect/rule/sqli/sqli_base_rule.rb +51 -0
  40. data/lib/contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions.rb +67 -0
  41. data/lib/contrast/agent/protect/rule/sqli.rb +6 -31
  42. data/lib/contrast/agent/protect/rule/xxe.rb +2 -0
  43. data/lib/contrast/agent/protect/rule.rb +3 -1
  44. data/lib/contrast/agent/reporting/attack_result/rasp_rule_sample.rb +6 -0
  45. data/lib/contrast/agent/reporting/details/sqli_dangerous_functions.rb +22 -0
  46. data/lib/contrast/agent/reporting/reporter.rb +1 -2
  47. data/lib/contrast/agent/reporting/reporting_events/agent_startup.rb +2 -2
  48. data/lib/contrast/agent/reporting/reporting_events/application_activity.rb +1 -4
  49. data/lib/contrast/agent/reporting/reporting_events/application_startup.rb +1 -1
  50. data/lib/contrast/agent/reporting/reporting_events/architecture_component.rb +0 -23
  51. data/lib/contrast/agent/reporting/reporting_events/finding.rb +19 -49
  52. data/lib/contrast/agent/reporting/reporting_events/finding_event.rb +12 -9
  53. data/lib/contrast/agent/reporting/reporting_events/finding_event_signature.rb +1 -1
  54. data/lib/contrast/agent/reporting/reporting_events/finding_event_source.rb +23 -21
  55. data/lib/contrast/agent/reporting/reporting_events/finding_event_stack.rb +5 -18
  56. data/lib/contrast/agent/reporting/reporting_events/finding_event_taint_range.rb +1 -0
  57. data/lib/contrast/{api/decorators/trace_taint_range_tags.rb → agent/reporting/reporting_events/finding_event_taint_range_tags.rb} +7 -6
  58. data/lib/contrast/agent/reporting/reporting_events/finding_request.rb +1 -1
  59. data/lib/contrast/agent/reporting/reporting_events/library_usage_observation.rb +1 -1
  60. data/lib/contrast/agent/reporting/reporting_events/observed_route.rb +2 -2
  61. data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +10 -14
  62. data/lib/contrast/agent/reporting/reporting_events/reporting_event.rb +11 -0
  63. data/lib/contrast/agent/reporting/reporting_events/route_coverage.rb +3 -1
  64. data/lib/contrast/agent/reporting/reporting_events/route_discovery.rb +11 -23
  65. data/lib/contrast/agent/reporting/reporting_events/route_discovery_observation.rb +8 -26
  66. data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +1 -1
  67. data/lib/contrast/agent/reporting/reporting_utilities/build_preflight.rb +4 -7
  68. data/lib/contrast/agent/reporting/reporting_utilities/headers.rb +1 -1
  69. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +3 -3
  70. data/lib/contrast/agent/request.rb +2 -2
  71. data/lib/contrast/agent/request_context.rb +8 -20
  72. data/lib/contrast/agent/request_context_extend.rb +15 -36
  73. data/lib/contrast/agent/request_handler.rb +0 -8
  74. data/lib/contrast/agent/response.rb +0 -18
  75. data/lib/contrast/agent/telemetry/events/event.rb +1 -1
  76. data/lib/contrast/agent/telemetry/events/metric_event.rb +1 -1
  77. data/lib/contrast/agent/telemetry/events/startup_metrics_event.rb +3 -3
  78. data/lib/contrast/agent/version.rb +1 -1
  79. data/lib/contrast/api/communication/messaging_queue.rb +2 -3
  80. data/lib/contrast/api/communication/socket_client.rb +4 -4
  81. data/lib/contrast/api/communication/speedracer.rb +4 -8
  82. data/lib/contrast/api/decorators/agent_startup.rb +5 -6
  83. data/lib/contrast/api/decorators/application_settings.rb +2 -1
  84. data/lib/contrast/api/decorators/application_startup.rb +6 -6
  85. data/lib/contrast/api/decorators/message.rb +0 -4
  86. data/lib/contrast/api/decorators/rasp_rule_sample.rb +0 -6
  87. data/lib/contrast/api/decorators.rb +0 -6
  88. data/lib/contrast/api/dtm.pb.rb +0 -489
  89. data/lib/contrast/components/agent.rb +16 -12
  90. data/lib/contrast/components/api.rb +10 -10
  91. data/lib/contrast/components/app_context.rb +3 -3
  92. data/lib/contrast/components/app_context_extend.rb +1 -1
  93. data/lib/contrast/components/assess.rb +92 -38
  94. data/lib/contrast/components/assess_rules.rb +36 -0
  95. data/lib/contrast/components/config.rb +54 -12
  96. data/lib/contrast/components/contrast_service.rb +8 -8
  97. data/lib/contrast/components/heap_dump.rb +1 -1
  98. data/lib/contrast/components/protect.rb +5 -5
  99. data/lib/contrast/components/ruby_component.rb +81 -0
  100. data/lib/contrast/components/sampling.rb +1 -1
  101. data/lib/contrast/components/security_logger.rb +23 -0
  102. data/lib/contrast/components/service.rb +55 -0
  103. data/lib/contrast/components/settings.rb +12 -4
  104. data/lib/contrast/config/base_configuration.rb +1 -1
  105. data/lib/contrast/config/protect_rules_configuration.rb +17 -3
  106. data/lib/contrast/config/server_configuration.rb +1 -1
  107. data/lib/contrast/config.rb +0 -6
  108. data/lib/contrast/configuration.rb +81 -17
  109. data/lib/contrast/extension/assess/exec_trigger.rb +3 -1
  110. data/lib/contrast/extension/assess/marshal.rb +3 -2
  111. data/lib/contrast/extension/assess/string.rb +0 -1
  112. data/lib/contrast/extension/extension.rb +1 -1
  113. data/lib/contrast/framework/base_support.rb +0 -5
  114. data/lib/contrast/framework/grape/support.rb +1 -23
  115. data/lib/contrast/framework/manager.rb +0 -10
  116. data/lib/contrast/framework/rails/support.rb +5 -58
  117. data/lib/contrast/framework/sinatra/support.rb +2 -21
  118. data/lib/contrast/logger/cef_log.rb +21 -3
  119. data/lib/contrast/logger/log.rb +1 -11
  120. data/lib/contrast/tasks/config.rb +4 -2
  121. data/lib/contrast/utils/assess/event_limit_utils.rb +5 -8
  122. data/lib/contrast/utils/assess/trigger_method_utils.rb +10 -18
  123. data/lib/contrast/utils/findings.rb +6 -5
  124. data/lib/contrast/utils/hash_digest.rb +9 -24
  125. data/lib/contrast/utils/hash_digest_extend.rb +6 -6
  126. data/lib/contrast/utils/invalid_configuration_util.rb +21 -58
  127. data/lib/contrast/utils/log_utils.rb +32 -8
  128. data/lib/contrast/utils/net_http_base.rb +2 -2
  129. data/lib/contrast/utils/patching/policy/patch_utils.rb +3 -2
  130. data/lib/contrast/utils/stack_trace_utils.rb +0 -25
  131. data/lib/contrast/utils/string_utils.rb +9 -0
  132. data/lib/contrast/utils/telemetry_client.rb +13 -7
  133. data/lib/contrast.rb +5 -10
  134. metadata +22 -28
  135. data/lib/contrast/agent/reporting/reporting_events/trace_event_source.rb +0 -30
  136. data/lib/contrast/agent/reporting/reporting_utilities/dtm_message.rb +0 -36
  137. data/lib/contrast/api/decorators/activity.rb +0 -33
  138. data/lib/contrast/api/decorators/architecture_component.rb +0 -36
  139. data/lib/contrast/api/decorators/finding.rb +0 -29
  140. data/lib/contrast/api/decorators/route_coverage.rb +0 -91
  141. data/lib/contrast/api/decorators/trace_event.rb +0 -120
  142. data/lib/contrast/api/decorators/trace_event_object.rb +0 -63
  143. data/lib/contrast/api/decorators/trace_event_signature.rb +0 -69
  144. data/lib/contrast/api/decorators/trace_taint_range.rb +0 -52
  145. data/lib/contrast/config/assess_configuration.rb +0 -93
  146. data/lib/contrast/config/assess_rules_configuration.rb +0 -32
  147. data/lib/contrast/config/root_configuration.rb +0 -90
  148. data/lib/contrast/config/ruby_configuration.rb +0 -81
  149. data/lib/contrast/config/service_configuration.rb +0 -49
  150. 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.response = Contrast::Agent::Reporting::ResponseType::MONITORED
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
@@ -29,6 +29,7 @@ module Contrast
29
29
  end
30
30
 
31
31
  return false if protect_excluded_by_code?
32
+ return false if protect_excluded_by_url?(context)
32
33
 
33
34
  true
34
35
  end
@@ -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/reporting/details/cmd_injection_details'
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::BaseService
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
- CHAINED_COMMAND_CHARS = /[;&|<>]/.cs__freeze
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
- raise(Contrast::SecurityException.new(self,
67
- 'Command Injection rule triggered. '\
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? _context
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