contrast-agent 4.2.0 → 4.3.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -0
  3. data/ext/cs__assess_marshal_module/cs__assess_marshal_module.c +22 -10
  4. data/ext/cs__assess_marshal_module/cs__assess_marshal_module.h +4 -3
  5. data/lib/contrast/agent/assess/contrast_event.rb +49 -130
  6. data/lib/contrast/agent/assess/contrast_object.rb +51 -0
  7. data/lib/contrast/agent/assess/events/source_event.rb +4 -9
  8. data/lib/contrast/agent/assess/policy/patcher.rb +4 -3
  9. data/lib/contrast/agent/assess/policy/policy_node.rb +31 -59
  10. data/lib/contrast/agent/assess/policy/preshift.rb +3 -3
  11. data/lib/contrast/agent/assess/policy/propagation_method.rb +13 -19
  12. data/lib/contrast/agent/assess/policy/propagation_node.rb +12 -24
  13. data/lib/contrast/agent/assess/policy/propagator/append.rb +1 -2
  14. data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -2
  15. data/lib/contrast/agent/assess/policy/propagator/custom.rb +1 -1
  16. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +1 -3
  17. data/lib/contrast/agent/assess/policy/propagator/insert.rb +1 -2
  18. data/lib/contrast/agent/assess/policy/propagator/keep.rb +1 -2
  19. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +3 -2
  20. data/lib/contrast/agent/assess/policy/propagator/next.rb +1 -2
  21. data/lib/contrast/agent/assess/policy/propagator/prepend.rb +1 -2
  22. data/lib/contrast/agent/assess/policy/propagator/remove.rb +2 -4
  23. data/lib/contrast/agent/assess/policy/propagator/replace.rb +1 -2
  24. data/lib/contrast/agent/assess/policy/propagator/reverse.rb +1 -2
  25. data/lib/contrast/agent/assess/policy/propagator/select.rb +3 -4
  26. data/lib/contrast/agent/assess/policy/propagator/splat.rb +2 -4
  27. data/lib/contrast/agent/assess/policy/propagator/split.rb +73 -117
  28. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +11 -11
  29. data/lib/contrast/agent/assess/policy/propagator/trim.rb +3 -7
  30. data/lib/contrast/agent/assess/policy/source_method.rb +2 -14
  31. data/lib/contrast/agent/assess/policy/trigger/reflected_xss.rb +5 -8
  32. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +1 -1
  33. data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +1 -1
  34. data/lib/contrast/agent/assess/property/tagged.rb +21 -15
  35. data/lib/contrast/agent/assess/rule/redos.rb +1 -1
  36. data/lib/contrast/agent/assess/tracker.rb +16 -18
  37. data/lib/contrast/agent/deadzone/policy/deadzone_node.rb +7 -0
  38. data/lib/contrast/agent/middleware.rb +50 -1
  39. data/lib/contrast/agent/patching/policy/method_policy.rb +1 -1
  40. data/lib/contrast/agent/patching/policy/patch.rb +4 -4
  41. data/lib/contrast/agent/protect/policy/applies_deserialization_rule.rb +47 -1
  42. data/lib/contrast/agent/protect/policy/rule_applicator.rb +53 -0
  43. data/lib/contrast/agent/protect/rule/base.rb +63 -14
  44. data/lib/contrast/agent/protect/rule/cmd_injection.rb +3 -3
  45. data/lib/contrast/agent/protect/rule/default_scanner.rb +1 -4
  46. data/lib/contrast/agent/protect/rule/deserialization.rb +4 -1
  47. data/lib/contrast/agent/protect/rule/no_sqli.rb +3 -3
  48. data/lib/contrast/agent/protect/rule/sqli.rb +3 -3
  49. data/lib/contrast/agent/protect/rule/xxe.rb +32 -11
  50. data/lib/contrast/agent/protect/rule/xxe/entity_wrapper.rb +10 -6
  51. data/lib/contrast/agent/reaction_processor.rb +1 -1
  52. data/lib/contrast/agent/response.rb +5 -5
  53. data/lib/contrast/agent/rewriter.rb +3 -3
  54. data/lib/contrast/agent/scope.rb +33 -13
  55. data/lib/contrast/agent/static_analysis.rb +13 -7
  56. data/lib/contrast/agent/version.rb +1 -1
  57. data/lib/contrast/api/decorators/library.rb +1 -0
  58. data/lib/contrast/api/decorators/library_usage_update.rb +1 -0
  59. data/lib/contrast/api/decorators/trace_event.rb +19 -31
  60. data/lib/contrast/api/decorators/trace_event_object.rb +11 -3
  61. data/lib/contrast/api/decorators/trace_event_signature.rb +27 -5
  62. data/lib/contrast/api/decorators/user_input.rb +2 -1
  63. data/lib/contrast/common_agent_configuration.rb +1 -1
  64. data/lib/contrast/components/assess.rb +36 -0
  65. data/lib/contrast/components/interface.rb +5 -3
  66. data/lib/contrast/components/scope.rb +23 -0
  67. data/lib/contrast/components/settings.rb +3 -3
  68. data/lib/contrast/config/assess_configuration.rb +2 -1
  69. data/lib/contrast/extension/assess/array.rb +1 -2
  70. data/lib/contrast/extension/assess/erb.rb +1 -3
  71. data/lib/contrast/extension/assess/exec_trigger.rb +1 -1
  72. data/lib/contrast/extension/assess/fiber.rb +2 -3
  73. data/lib/contrast/extension/assess/hash.rb +4 -2
  74. data/lib/contrast/extension/assess/kernel.rb +1 -2
  75. data/lib/contrast/extension/assess/marshal.rb +34 -26
  76. data/lib/contrast/extension/assess/regexp.rb +3 -8
  77. data/lib/contrast/extension/assess/string.rb +1 -2
  78. data/lib/contrast/framework/base_support.rb +51 -53
  79. data/lib/contrast/framework/manager.rb +3 -2
  80. data/lib/contrast/framework/rack/patch/session_cookie.rb +1 -1
  81. data/lib/contrast/framework/rack/support.rb +2 -1
  82. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +1 -1
  83. data/lib/contrast/framework/rails/patch/rails_application_configuration.rb +1 -1
  84. data/lib/contrast/framework/rails/rewrite/action_controller_railties_helper_inherited.rb +1 -1
  85. data/lib/contrast/framework/rails/rewrite/active_record_attribute_methods_read.rb +1 -1
  86. data/lib/contrast/framework/rails/rewrite/active_record_time_zone_inherited.rb +1 -1
  87. data/lib/contrast/framework/rails/support.rb +2 -1
  88. data/lib/contrast/framework/sinatra/support.rb +3 -2
  89. data/lib/contrast/logger/application.rb +0 -3
  90. data/lib/contrast/utils/duck_utils.rb +1 -1
  91. data/lib/contrast/utils/heap_dump_util.rb +1 -1
  92. data/lib/contrast/utils/object_share.rb +3 -3
  93. data/lib/contrast/utils/preflight_util.rb +1 -1
  94. data/lib/contrast/utils/prevent_serialization.rb +1 -1
  95. data/lib/contrast/utils/resource_loader.rb +1 -1
  96. data/lib/contrast/utils/sha256_builder.rb +2 -2
  97. data/lib/contrast/utils/string_utils.rb +1 -1
  98. data/lib/contrast/utils/tag_util.rb +9 -13
  99. data/resources/assess/policy.json +9 -9
  100. data/resources/deadzone/policy.json +156 -0
  101. data/resources/protect/policy.json +12 -0
  102. data/ruby-agent.gemspec +9 -6
  103. data/service_executables/VERSION +1 -1
  104. data/service_executables/linux/contrast-service +0 -0
  105. data/service_executables/mac/contrast-service +0 -0
  106. metadata +68 -25
@@ -92,8 +92,7 @@ module Contrast
92
92
  end
93
93
 
94
94
  def string_sub parent_events, self_tracked, preshift, ret, incoming, incoming_tracked, global
95
- properties = Contrast::Agent::Assess::Tracker.properties(ret)
96
- return unless properties
95
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))
97
96
 
98
97
  incoming_properties = Contrast::Agent::Assess::Tracker.properties(incoming)
99
98
  parent_event = incoming_properties&.event
@@ -141,22 +140,23 @@ module Contrast
141
140
  end
142
141
 
143
142
  def block_sub self_tracked, source, ret
144
- properties = Contrast::Agent::Assess::Tracker.properties(ret)
145
- properties&.splat_from(source, ret) if self_tracked
143
+ return unless self_tracked
144
+
145
+ properties = Contrast::Agent::Assess::Tracker.properties!(ret)
146
+ properties&.splat_from(source, ret)
146
147
  end
147
148
 
148
149
  def hash_sub self_tracked, source, ret
149
- properties = Contrast::Agent::Assess::Tracker.properties(ret)
150
- properties&.splat_from(source, ret) if self_tracked
150
+ return unless self_tracked
151
+
152
+ properties = Contrast::Agent::Assess::Tracker.properties!(ret)
153
+ properties&.splat_from(source, ret)
151
154
  end
152
155
 
153
156
  def pattern_gsub parent_events, preshift, ret
154
- properties = Contrast::Agent::Assess::Tracker.properties(ret)
155
- return unless properties
156
-
157
157
  source = preshift.object
158
- source_properties = Contrast::Agent::Assess::Tracker.properties(source)
159
- return unless source_properties
158
+ return unless (source_properties = Contrast::Agent::Assess::Tracker.properties(source))
159
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))
160
160
 
161
161
  source_properties.tag_keys.each do |key|
162
162
  properties.add_tag(key, 0...1)
@@ -11,13 +11,11 @@ module Contrast
11
11
  # Disclaimer: there may be a better way, but we're
12
12
  # in a 'get it work' state. hopefully, we'll be in
13
13
  # a 'get it right' state soon.
14
- class Trim
14
+ module Trim
15
15
  class << self
16
16
  def tr_tagger patcher, preshift, ret, _block
17
17
  return ret unless ret && !ret.empty?
18
-
19
- properties = Contrast::Agent::Assess::Tracker.properties(ret)
20
- return unless properties
18
+ return ret unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))
21
19
 
22
20
  source = preshift.object
23
21
  args = preshift.args
@@ -59,9 +57,7 @@ module Contrast
59
57
 
60
58
  def tr_s_tagger patcher, preshift, ret, _block
61
59
  return unless ret && !ret.empty?
62
-
63
- properties = Contrast::Agent::Assess::Tracker.properties(ret)
64
- return unless properties
60
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))
65
61
 
66
62
  source = preshift.object
67
63
  args = preshift.args
@@ -178,8 +178,8 @@ module Contrast
178
178
  # don't apply second source -- probably needs tuning later if we
179
179
  # use more than 'UNTRUSTED' in our sources
180
180
  return if Contrast::Agent::Assess::Tracker.tracked?(target)
181
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
181
182
 
182
- properties = Contrast::Agent::Assess::Tracker.properties(target)
183
183
  # otherwise for each tag this source_node applies, create a tag range
184
184
  # on the target object
185
185
  # I realize this looping is counter-intuitive from the above
@@ -243,19 +243,7 @@ module Contrast
243
243
  when Contrast::Utils::ObjectShare::OBJECT_KEY
244
244
  object
245
245
  else
246
- if source_target.is_a?(Integer)
247
- args[source_target]
248
- # If this isn't an index param, it's a named one. R.I.P.
249
- else
250
- arg = nil
251
- args.each do |search|
252
- next unless search.is_a?(Hash)
253
-
254
- arg = search[source_target]
255
- break if arg
256
- end
257
- arg
258
- end
246
+ args[source_target]
259
247
  end
260
248
  end
261
249
 
@@ -25,14 +25,13 @@ module Contrast
25
25
  TEMPLATE_PROPAGATION_NODE = Contrast::Agent::Assess::Policy::PropagationNode.new(NODE_HASH)
26
26
 
27
27
  def xss_tilt_trigger context, trigger_node, _source, object, ret, *args
28
- properties = Contrast::Agent::Assess::Tracker.properties(ret)
29
- return unless properties
28
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))
30
29
 
31
30
  scope = args[0]
32
31
  erb_template_prerender = object.instance_variable_get(:@data)
33
32
  interpolated_inputs = []
34
- handle_binding_variables(scope, erb_template_prerender, ret, interpolated_inputs)
35
- handle_local_variables(args, erb_template_prerender, ret, interpolated_inputs)
33
+ handle_binding_variables(scope, erb_template_prerender, ret, properties, interpolated_inputs)
34
+ handle_local_variables(args, erb_template_prerender, ret, properties, interpolated_inputs)
36
35
  properties.build_event(TEMPLATE_PROPAGATION_NODE, ret, erb_template_prerender, ret, interpolated_inputs)
37
36
  unless interpolated_inputs.empty?
38
37
  current_event = properties.event
@@ -53,8 +52,7 @@ module Contrast
53
52
 
54
53
  private
55
54
 
56
- def handle_binding_variables scope, erb_template_prerender, ret, interpolated_inputs
57
- properties = Contrast::Agent::Assess::Tracker.properties(ret)
55
+ def handle_binding_variables scope, erb_template_prerender, ret, properties, interpolated_inputs
58
56
  binding_variables = scope.instance_variables
59
57
 
60
58
  binding_variables.each do |bound_variable_sym|
@@ -71,8 +69,7 @@ module Contrast
71
69
  end
72
70
  end
73
71
 
74
- def handle_local_variables args, erb_template_prerender, ret, interpolated_inputs
75
- properties = Contrast::Agent::Assess::Tracker.properties(ret)
72
+ def handle_local_variables args, erb_template_prerender, ret, properties, interpolated_inputs
76
73
  locals = args[1]
77
74
  locals.each do |local_name, local_value|
78
75
  next unless Contrast::Agent::Assess::Tracker.tracked?(local_value)
@@ -43,7 +43,7 @@ module Contrast
43
43
  next unless trigger_node.violated?(arg)
44
44
 
45
45
  Contrast::Agent::Assess::Policy::TriggerMethod.build_finding(
46
- context, trigger_node, arg, object, ret, args)
46
+ context, trigger_node, arg, object, ret, *args)
47
47
  end
48
48
 
49
49
  ret
@@ -39,7 +39,7 @@ module Contrast
39
39
  finish ||= url.length
40
40
 
41
41
  properties = Contrast::Agent::Assess::Tracker.properties(args[0])
42
- properties.any_tags_between?(start, finish)
42
+ properties&.any_tags_between?(start, finish)
43
43
  end
44
44
  end
45
45
  end
@@ -79,6 +79,10 @@ module Contrast
79
79
  # range : 5-10
80
80
  # result : 0-05
81
81
  #
82
+ # Note that we disable Lint/DuplicateBranch in this branch in order
83
+ # list out all tag range cases in the proper order to make this
84
+ # easier to understand
85
+ #
82
86
  # @param range [Range] the span to check, inclusive to exclusive
83
87
  # @return [Hash{String => Contrast::Agent::Assess::Tag}] the hash of
84
88
  # key to tags
@@ -101,10 +105,10 @@ module Contrast
101
105
  finish = range.size - start
102
106
  add << Contrast::Agent::Assess::Tag.new(tag.label, finish, start)
103
107
  # the tag spans the requested range.
104
- when Contrast::Agent::Assess::Tag::WITHOUT
108
+ when Contrast::Agent::Assess::Tag::WITHOUT # rubocop:disable Lint/DuplicateBranch
105
109
  add << Contrast::Agent::Assess::Tag.new(tag.label, range.size)
106
110
  # part of the tag is being selected
107
- when Contrast::Agent::Assess::Tag::HIGH_SPAN
111
+ when Contrast::Agent::Assess::Tag::HIGH_SPAN # rubocop:disable Lint/DuplicateBranch
108
112
  add << Contrast::Agent::Assess::Tag.new(tag.label, range.size)
109
113
  end
110
114
  end
@@ -173,14 +177,6 @@ module Contrast
173
177
  tags.fetch(label, nil) if tracked?
174
178
  end
175
179
 
176
- # Convert the tags of this object into the TraceTaintRange required
177
- # to be sent to the service
178
- #
179
- # @return [Array<Contrast::Api::Dtm::TraceTaintRange>]
180
- def tags_to_dtm
181
- Contrast::Api::Dtm::TraceTaintRange.build_for_event(tags)
182
- end
183
-
184
180
  # Remove all tags within the given ranges.
185
181
  # This does not delete an entire tag if part of that tag is
186
182
  # outside this range, meaning we may reduce sizes of tags
@@ -301,9 +297,19 @@ module Contrast
301
297
  end
302
298
  end
303
299
 
304
- # Shift the tag ranges covering the given range
305
- # We assume this is for a insertion, meaning we
306
- # have to move tags to the right
300
+ # Shift the tag ranges covering the given range to account for new
301
+ # data being added. We assume this is for a insertion, meaning we
302
+ # have to move tags out of the range and to the right. For example,
303
+ # given current tags: 0-15
304
+ # when we insert a range: 5-10
305
+ # then the result is: 0-5, 10-20
306
+ #
307
+ # Note that we disable Lint/DuplicateBranch in this branch in order
308
+ # list out all tag range cases in the proper order to make this
309
+ # easier to understand
310
+ #
311
+ # @param range [Range] the range of new information that's been
312
+ # inserted
307
313
  def shift_tags_for_insertion range
308
314
  return unless tracked?
309
315
 
@@ -325,13 +331,13 @@ module Contrast
325
331
  when Contrast::Agent::Assess::Tag::WITHIN
326
332
  tag.shift(length)
327
333
  # the tag spans the range. leave the part below alone
328
- when Contrast::Agent::Assess::Tag::WITHOUT
334
+ when Contrast::Agent::Assess::Tag::WITHOUT # rubocop:disable Lint/DuplicateBranch
329
335
  new_tag = tag.clone
330
336
  new_tag.update_start(range.begin)
331
337
  new_tag.shift(length)
332
338
  add << new_tag
333
339
  tag.update_end(range.begin)
334
- when Contrast::Agent::Assess::Tag::HIGH_SPAN, Contrast::Agent::Assess::Tag::ABOVE
340
+ when Contrast::Agent::Assess::Tag::HIGH_SPAN, Contrast::Agent::Assess::Tag::ABOVE # rubocop:disable Lint/DuplicateBranch
335
341
  tag.shift(length)
336
342
  end
337
343
  end
@@ -33,7 +33,7 @@ module Contrast
33
33
  # (2) regexp must evaluate against user input
34
34
  return unless trigger_node.violated?(string)
35
35
 
36
- Contrast::Agent::Assess::Policy::TriggerMethod.build_finding(context, trigger_node, source, object, ret, args)
36
+ Contrast::Agent::Assess::Policy::TriggerMethod.build_finding(context, trigger_node, source, object, ret, *args)
37
37
  end
38
38
 
39
39
  protected
@@ -14,7 +14,20 @@ module Contrast
14
14
  PROPERTIES_HASH = Contrast::Agent::Assess::Finalizers::Hash.new
15
15
 
16
16
  class << self
17
+ # Retrieve the properties of the given Object, iff they exist.
18
+ #
19
+ # @param source [Object] the thing for which to look up properties.
20
+ # @return [Contrast::Agent::Assess::Properties, nil]
17
21
  def properties source
22
+ PROPERTIES_HASH[source]
23
+ end
24
+
25
+ # Retrieve the properties of the given Object, returning a new
26
+ # instance if one does not already exist and the source is trackable.
27
+ #
28
+ # @param source [Object] the thing for which to look up properties.
29
+ # @return [Contrast::Agent::Assess::Properties, nil]
30
+ def properties! source
18
31
  return unless trackable?(source)
19
32
 
20
33
  PROPERTIES_HASH[source] ||= Contrast::Agent::Assess::Properties.new
@@ -33,30 +46,15 @@ module Contrast
33
46
  end
34
47
 
35
48
  # Copy the properties from one object to the next, assuming the
36
- # target does not already have its own properties.
49
+ # target does not already have its own properties. This should only
50
+ # ever be called when building PreShift for those objects sources
51
+ # which are already tracked.
37
52
  #
38
53
  # @param source [Object] the instance from which to copy properties
39
54
  # @param target [Object] the instance to which to copy properties
40
55
  def copy source, target
41
56
  PROPERTIES_HASH[target] ||= properties(source).dup
42
57
  end
43
-
44
- # Duplicate the given object, returning the duplicate after copying
45
- # the properties of the original and storing them as the properties
46
- # of the duplicate.
47
- #
48
- # @param source [Object] the thing to duplicate
49
- # @return [Object] the duplicate of the original, or the original if
50
- # it does not respond to duplication
51
- def duplicate source
52
- return source unless source
53
-
54
- duplicate = source.dup
55
- PROPERTIES_HASH[duplicate] ||= PROPERTIES_HASH[source].dup
56
- duplicate
57
- rescue StandardError
58
- source
59
- end
60
58
  end
61
59
  end
62
60
  end
@@ -19,6 +19,13 @@ module Contrast
19
19
  def feature
20
20
  'Deadzone'
21
21
  end
22
+
23
+ # Deadzone means place the code inside of the contrast scope so that
24
+ # none of our code executes. As such, all DeadZone nodes have that as
25
+ # their method scope
26
+ def method_scope
27
+ :contrast
28
+ end
22
29
  end
23
30
  end
24
31
  end
@@ -79,7 +79,7 @@ module Contrast
79
79
  Contrast::Utils::HeapDumpUtil.run
80
80
 
81
81
  if AGENT.enabled?
82
- Contrast::Agent::StaticAnalysis.catchup
82
+ handle_first_request
83
83
  call_with_agent(env)
84
84
  else
85
85
  app.call(env)
@@ -91,6 +91,8 @@ module Contrast
91
91
  def setup_agent
92
92
  SETTINGS.reset_state
93
93
 
94
+ inform_deprecations
95
+
94
96
  if CONFIG.invalid?
95
97
  AGENT.disable!
96
98
  logger.error('!!! CONFIG FILE IS INVALID - DISABLING CONTRAST AGENT !!!')
@@ -101,6 +103,35 @@ module Contrast
101
103
  end
102
104
  end
103
105
 
106
+ # Some things have to wait until first request to happen, either because
107
+ # resolution is not complete or because the framework will preload
108
+ # classes, which confuses some of our instrumentation.
109
+ def handle_first_request
110
+ @_handle_first_request ||= begin
111
+ Contrast::Agent::StaticAnalysis.catchup
112
+ force_patching
113
+ true
114
+ end
115
+ end
116
+
117
+ # TODO: RUBY-1090 remove this method and those it calls.
118
+ #
119
+ # These modules are auto-loaded by Rails, meaning they are defined at
120
+ # startup, but that they don't actually exist. We account for this in
121
+ # most cases by using the ClassUtil.truly_defined? method, but it appears
122
+ # to fail for these Modules. In the short term, we can forcing a re-patch
123
+ # so that their dead zones apply.
124
+ def force_patching
125
+ force_patch(ActionDispatch::FileHandler) if defined?(ActionDispatch::FileHandler)
126
+ force_patch(ActionDispatch::Http::MimeNegotiation) if defined?(ActionDispatch::Http::MimeNegotiation)
127
+ force_patch(ActionView::Template) if defined?(ActionView::Template)
128
+ end
129
+
130
+ def force_patch mod
131
+ data = Contrast::Agent::ModuleData.new(mod)
132
+ Contrast::Agent::Patching::Policy::Patcher.send(:patch_into_module, data, true)
133
+ end
134
+
104
135
  # This is where we process each request we intercept as a middleware. We make the request context
105
136
  # available globally so that it can be accessed from anywhere. A RequestHandler object is made
106
137
  # for each request, which handles prefilter and postfilter operations.
@@ -173,6 +204,24 @@ module Contrast
173
204
  raise exception
174
205
  end
175
206
  end
207
+
208
+ # As we deprecate support to prepare to remove dead code, we need to
209
+ # inform our users still relying on the now deprecated and soon to be
210
+ # removed functionality. This method handles doing that by leveraging the
211
+ # standard Kernel#warn approach
212
+ def inform_deprecations
213
+ # Ruby 2.5 is currently in security maintenance, meaning int is only
214
+ # receiving updates for security issues. It will move to eol on 31
215
+ # March 2021. As such, we can remove support for it in Q2. We'll begin
216
+ # the deprecation warnings now so that customers have time to reach out
217
+ # if they'll be impacted.
218
+ # TODO: RUBY-715 remove this part of the method, leaving it empty if
219
+ # there are no other deprecations, when we drop 2.5 support.
220
+ return unless RUBY_VERSION < '2.6.0'
221
+
222
+ Kernel.warn('[Contrast Security] [DEPRECATION] Support for Ruby 2.5 will be removed in April 2021. '\
223
+ 'Please contact Customer Support prior if you require continued support.')
224
+ end
176
225
  end
177
226
  end
178
227
  end
@@ -89,7 +89,7 @@ module Contrast
89
89
  end
90
90
 
91
91
  def find_method_node nodes, method_name, is_instance_method
92
- return nil unless nodes
92
+ return unless nodes
93
93
 
94
94
  nodes.find do |node|
95
95
  node.instance_method? == is_instance_method && node.method_name == method_name
@@ -366,17 +366,17 @@ module Contrast
366
366
  # alias_method may be private
367
367
  target_module.send(:alias_method, underlying_method_name, method_name)
368
368
  # TODO: RUBY-1052
369
- # rubocop:disable Kernel/DefineMethod
369
+ # rubocop:disable Performance/Kernel/DefineMethod
370
370
  target_module.send(:define_method, method_name, unbound_method.bind(target_module))
371
- # rubocop:enable Kernel/DefineMethod
371
+ # rubocop:enable Performance/Kernel/DefineMethod
372
372
  end
373
373
  target_module.send(visibility, method_name) # e.g., module.private(:my_method)
374
374
  when :prepend
375
375
  prepending_module = Module.new
376
376
  # TODO: RUBY-1052
377
- # rubocop:disable Kernel/DefineMethod
377
+ # rubocop:disable Performance/Kernel/DefineMethod
378
378
  prepending_module.send(:define_method, method_name, unbound_method.bind(target_module))
379
- # rubocop:enable Kernel/DefineMethod
379
+ # rubocop:enable Performance/Kernel/DefineMethod
380
380
  prepending_module.send(visibility, method_name)
381
381
  # This prepends to the singleton class (it patches a class method)
382
382
  target_module.prepend prepending_module
@@ -16,6 +16,23 @@ module Contrast
16
16
  extend Contrast::Agent::Protect::Policy::RuleApplicator
17
17
 
18
18
  class << self
19
+ # Calls the actual rule for this applicator, if required. Most rules
20
+ # invoke this from within their apply_rule method after doing
21
+ # whatever transformations they need to get into this common format.
22
+ #
23
+ # @param _method [Symbol] the name of the method for which this rule
24
+ # is invoked
25
+ # @param _exception [Exception] any exception raised; used for rules
26
+ # like Padding Oracle Attack (now defunct), which determine if the
27
+ # number and type of exceptions are an attack
28
+ # @param _properties [Hash] set of extra information provided by the
29
+ # applicator in an attempt to build a better story for the user
30
+ # @param _object [Object] the thing on which the triggering method
31
+ # was invoked
32
+ # @param args [Array<Object>] the arguments passed to the triggering
33
+ # method at invocation
34
+ # @raise [Contrast::SecurityException] on block, will pass the
35
+ # exception from the rule
19
36
  def invoke _method, _exception, _properties, _object, args
20
37
  return unless valid_input?(args)
21
38
  return if skip_analysis?
@@ -23,6 +40,28 @@ module Contrast
23
40
  rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, args[0])
24
41
  end
25
42
 
43
+ # Calls the actual rule for this applicator, if required, when the
44
+ # triggering method is called from Marshal.load when it has been
45
+ # prepended.
46
+ #
47
+ # @param arg [Object] the argument passed to the triggering method
48
+ # at invocation
49
+ # @raise [Contrast::SecurityException] on block, will pass the
50
+ # exception from the rule
51
+ def prepended_invoke arg
52
+ return unless arg&.cs__is_a?(String)
53
+ return if skip_analysis?
54
+
55
+ rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, arg)
56
+ end
57
+
58
+ # Allow the rule to check if the given input is an attempt to
59
+ # deserialize something in a way that will result in a command
60
+ # execution
61
+ #
62
+ # @param command [String] user input that potentially contains a
63
+ # Gadget, a Module that results in code execution on
64
+ # deserialization, and some form of command.
26
65
  def apply_deserialization_command_check command
27
66
  return unless command
28
67
  return if skip_analysis?
@@ -38,11 +77,18 @@ module Contrast
38
77
 
39
78
  private
40
79
 
80
+ # Determine if the rule would care about the arguments passed in
81
+ # this method invocation. Required b/c Ruby doesn't do type
82
+ # checking on its method signatures.
83
+ #
84
+ # @param args [Array<Object>] the arguments provided to the
85
+ # instrumented method
86
+ # @return [Boolean]
41
87
  def valid_input? args
42
88
  return false unless args&.any?
43
89
 
44
90
  input = args[0]
45
- input.is_a?(String)
91
+ input.cs__is_a?(String)
46
92
  end
47
93
  end
48
94
  end