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.
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