contrast-agent 3.8.5 → 3.9.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 (153) hide show
  1. checksums.yaml +4 -4
  2. data/ext/cs__assess_array/cs__assess_array.c +1 -1
  3. data/ext/cs__assess_module/cs__assess_module.c +0 -1
  4. data/ext/cs__assess_yield_track/cs__assess_yield_track.c +34 -0
  5. data/ext/cs__assess_yield_track/cs__assess_yield_track.h +12 -0
  6. data/ext/{cs__scope → cs__assess_yield_track}/extconf.rb +0 -0
  7. data/ext/cs__common/cs__common.c +6 -6
  8. data/ext/cs__common/cs__common.h +3 -1
  9. data/ext/cs__contrast_patch/cs__contrast_patch.c +142 -119
  10. data/ext/cs__contrast_patch/cs__contrast_patch.h +3 -0
  11. data/funchook/autom4te.cache/requests +48 -48
  12. data/funchook/config.log +2 -2
  13. data/lib/contrast/agent.rb +15 -5
  14. data/lib/contrast/agent/assess.rb +0 -1
  15. data/lib/contrast/agent/assess/contrast_event.rb +9 -8
  16. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +68 -18
  17. data/lib/contrast/agent/assess/policy/policy.rb +0 -14
  18. data/lib/contrast/agent/assess/policy/policy_scanner.rb +1 -1
  19. data/lib/contrast/agent/assess/policy/preshift.rb +1 -1
  20. data/lib/contrast/agent/assess/policy/propagation_method.rb +4 -2
  21. data/lib/contrast/agent/assess/policy/propagator/custom.rb +1 -1
  22. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +1 -1
  23. data/lib/contrast/agent/assess/policy/propagator/splat.rb +2 -2
  24. data/lib/contrast/agent/assess/policy/propagator/split.rb +166 -1
  25. data/lib/contrast/agent/assess/policy/rewriter_patch.rb +1 -0
  26. data/lib/contrast/agent/assess/policy/source_method.rb +199 -140
  27. data/lib/contrast/agent/assess/policy/source_validation/cross_site_validator.rb +30 -0
  28. data/lib/contrast/agent/assess/policy/source_validation/source_validation.rb +36 -0
  29. data/lib/contrast/agent/assess/policy/trigger_method.rb +238 -153
  30. data/lib/contrast/agent/assess/policy/trigger_node.rb +54 -9
  31. data/lib/contrast/agent/assess/policy/trigger_validation/trigger_validation.rb +13 -0
  32. data/lib/contrast/agent/assess/properties.rb +29 -0
  33. data/lib/contrast/agent/assess/rule/csrf/csrf_applicator.rb +35 -31
  34. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +1 -1
  35. data/lib/contrast/agent/class_reopener.rb +98 -55
  36. data/lib/contrast/agent/feature_state.rb +1 -1
  37. data/lib/contrast/agent/inventory/policy/policy.rb +1 -1
  38. data/lib/contrast/agent/logger_manager.rb +2 -2
  39. data/lib/contrast/agent/middleware.rb +1 -3
  40. data/lib/contrast/agent/patching/policy/after_load_patch.rb +40 -4
  41. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +33 -8
  42. data/lib/contrast/agent/patching/policy/method_policy.rb +20 -7
  43. data/lib/contrast/agent/patching/policy/patch.rb +54 -23
  44. data/lib/contrast/agent/patching/policy/patch_status.rb +0 -2
  45. data/lib/contrast/agent/patching/policy/patcher.rb +10 -11
  46. data/lib/contrast/agent/patching/policy/policy.rb +4 -0
  47. data/lib/contrast/agent/patching/policy/policy_node.rb +14 -1
  48. data/lib/contrast/agent/patching/policy/trigger_node.rb +2 -1
  49. data/lib/contrast/agent/protect/policy/policy.rb +6 -6
  50. data/lib/contrast/agent/protect/rule/base.rb +1 -1
  51. data/lib/contrast/agent/protect/rule/deserialization.rb +3 -25
  52. data/lib/contrast/agent/protect/rule/sqli.rb +1 -1
  53. data/lib/contrast/agent/railtie.rb +11 -5
  54. data/lib/contrast/agent/request.rb +1 -19
  55. data/lib/contrast/agent/request_context.rb +1 -1
  56. data/lib/contrast/agent/rewriter.rb +4 -3
  57. data/lib/contrast/agent/scope.rb +116 -19
  58. data/lib/contrast/agent/service_heartbeat.rb +5 -2
  59. data/lib/contrast/agent/settings_state.rb +12 -8
  60. data/lib/contrast/agent/version.rb +1 -1
  61. data/lib/contrast/api.rb +1 -0
  62. data/lib/contrast/api/speedracer.rb +2 -2
  63. data/lib/contrast/components/agent.rb +26 -7
  64. data/lib/contrast/components/app_context.rb +8 -45
  65. data/lib/contrast/components/contrast_service.rb +3 -4
  66. data/lib/contrast/components/interface.rb +1 -1
  67. data/lib/contrast/components/scope.rb +56 -26
  68. data/lib/contrast/config/ruby_configuration.rb +8 -3
  69. data/lib/contrast/delegators.rb +9 -0
  70. data/lib/contrast/delegators/application_update.rb +32 -0
  71. data/lib/contrast/extensions/framework/rack/cookie.rb +24 -0
  72. data/lib/contrast/extensions/framework/rack/request.rb +24 -0
  73. data/lib/contrast/extensions/framework/rack/response.rb +23 -0
  74. data/lib/contrast/extensions/framework/rails/action_controller_railties_helper_inherited.rb +20 -0
  75. data/lib/contrast/extensions/framework/rails/active_record.rb +26 -0
  76. data/lib/contrast/extensions/framework/rails/active_record_named.rb +53 -0
  77. data/lib/contrast/extensions/framework/rails/active_record_time_zone_inherited.rb +21 -0
  78. data/lib/contrast/extensions/framework/rails/buffer.rb +28 -0
  79. data/lib/contrast/extensions/framework/rails/configuration.rb +27 -0
  80. data/lib/contrast/extensions/framework/sinatra/base.rb +59 -0
  81. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess.rb +12 -11
  82. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/array.rb +4 -3
  83. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/assess_extension.rb +0 -2
  84. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/basic_object.rb +1 -1
  85. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/erb.rb +0 -0
  86. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/exec_trigger.rb +0 -0
  87. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/fiber.rb +3 -4
  88. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/hash.rb +0 -0
  89. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/kernel.rb +1 -1
  90. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/module.rb +1 -1
  91. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/regexp.rb +0 -0
  92. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/string.rb +0 -0
  93. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/tilt_template_trigger.rb +0 -0
  94. data/lib/contrast/extensions/ruby_core/assess/xpath_library_trigger.rb +40 -0
  95. data/lib/contrast/{core_extensions → extensions/ruby_core}/delegator.rb +0 -0
  96. data/lib/contrast/{core_extensions → extensions/ruby_core}/eval_trigger.rb +1 -1
  97. data/lib/contrast/{core_extensions → extensions/ruby_core}/inventory.rb +0 -0
  98. data/lib/contrast/{core_extensions → extensions/ruby_core}/inventory/datastores.rb +1 -1
  99. data/lib/contrast/extensions/ruby_core/module.rb +17 -0
  100. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect.rb +0 -0
  101. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_command_injection_rule.rb +8 -6
  102. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_deserialization_rule.rb +7 -5
  103. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_no_sqli_rule.rb +5 -3
  104. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_path_traversal_rule.rb +31 -27
  105. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_sqli_rule.rb +5 -3
  106. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_xxe_rule.rb +9 -7
  107. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/kernel.rb +0 -0
  108. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/psych.rb +1 -1
  109. data/lib/contrast/{core_extensions → extensions/ruby_core}/thread.rb +0 -0
  110. data/lib/contrast/framework/base_support.rb +63 -0
  111. data/lib/contrast/framework/manager.rb +109 -0
  112. data/lib/contrast/framework/platform_version.rb +21 -0
  113. data/lib/contrast/framework/rails_support.rb +88 -0
  114. data/lib/contrast/framework/sinatra_application_helper.rb +49 -0
  115. data/lib/contrast/framework/sinatra_support.rb +94 -0
  116. data/lib/contrast/framework/view_technologies_descriptor.rb +20 -0
  117. data/lib/contrast/utils/assess/tracking_util.rb +2 -4
  118. data/lib/contrast/utils/class_util.rb +92 -37
  119. data/lib/contrast/utils/duck_utils.rb +59 -39
  120. data/lib/contrast/utils/environment_util.rb +5 -75
  121. data/lib/contrast/utils/freeze_util.rb +3 -7
  122. data/lib/contrast/utils/invalid_configuration_util.rb +5 -5
  123. data/lib/contrast/utils/job_servers_running.rb +39 -0
  124. data/lib/contrast/utils/ruby_ast_rewriter.rb +2 -2
  125. data/lib/contrast/utils/service_response_util.rb +0 -6
  126. data/lib/contrast/utils/sinatra_helper.rb +6 -0
  127. data/lib/contrast/utils/stack_trace_utils.rb +1 -1
  128. data/resources/assess/policy.json +74 -23
  129. data/resources/inventory/policy.json +1 -1
  130. data/resources/protect/policy.json +11 -9
  131. data/resources/rubocops/object/frozen_cop.rb +1 -1
  132. data/ruby-agent.gemspec +2 -0
  133. data/service_executables/VERSION +1 -1
  134. data/service_executables/linux/contrast-service +0 -0
  135. data/service_executables/mac/contrast-service +0 -0
  136. metadata +94 -57
  137. data/ext/cs__scope/cs__scope.c +0 -96
  138. data/ext/cs__scope/cs__scope.h +0 -33
  139. data/lib/contrast/agent/assess/class_reverter.rb +0 -82
  140. data/lib/contrast/agent/patching/policy/policy_unpatcher.rb +0 -28
  141. data/lib/contrast/core_extensions/module.rb +0 -42
  142. data/lib/contrast/core_extensions/object.rb +0 -27
  143. data/lib/contrast/rails_extensions/assess/action_controller_inheritance.rb +0 -48
  144. data/lib/contrast/rails_extensions/assess/active_record.rb +0 -32
  145. data/lib/contrast/rails_extensions/assess/active_record_named.rb +0 -61
  146. data/lib/contrast/rails_extensions/assess/configuration.rb +0 -26
  147. data/lib/contrast/rails_extensions/buffer.rb +0 -30
  148. data/lib/contrast/rails_extensions/rack.rb +0 -45
  149. data/lib/contrast/sinatra_extensions/assess/cookie.rb +0 -26
  150. data/lib/contrast/sinatra_extensions/inventory/sinatra_base.rb +0 -59
  151. data/lib/contrast/utils/operating_environment.rb +0 -38
  152. data/lib/contrast/utils/path_util.rb +0 -151
  153. data/lib/contrast/utils/scope_util.rb +0 -99
@@ -22,12 +22,12 @@ module Contrast
22
22
  arg = preshift.args[source]
23
23
  if arg.is_a?(String)
24
24
  tracked_inputs << arg if arg.cs__tracked?
25
- elsif Contrast::Utils::DuckUtils.quacks_like_tracked_hash?(arg)
25
+ elsif Contrast::Utils::DuckUtils.iterable_hash?(arg)
26
26
  arg.each_pair do |key, value|
27
27
  tracked_inputs << key if tracked_value?(key)
28
28
  tracked_inputs << value if tracked_value?(value)
29
29
  end
30
- elsif Contrast::Utils::DuckUtils.quacks_like_tracked_enumerable?(arg)
30
+ elsif Contrast::Utils::DuckUtils.iterable_enumerable?(arg)
31
31
  arg.each do |value|
32
32
  tracked_inputs << value if tracked_value?(value)
33
33
  end
@@ -1,6 +1,10 @@
1
1
  # Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
2
  # frozen_string_literal: true
3
3
 
4
+ cs__scoped_require 'contrast/agent/assess/policy/preshift'
5
+ cs__scoped_require 'contrast/components/interface'
6
+ cs__scoped_require 'contrast/utils/thread_tracker'
7
+
4
8
  module Contrast
5
9
  module Agent
6
10
  module Assess
@@ -9,8 +13,15 @@ module Contrast
9
13
  # This class is specifically for String#split & String#grapheme_clusters propagation
10
14
  # it propagates tag ranges from a string to elements within an untracked array
11
15
  class Split < Contrast::Agent::Assess::Policy::Propagator::Base
16
+ include Contrast::Components::Interface
17
+
18
+ access_component :agent, :logging
19
+
20
+ SPLIT_TRACKER = Contrast::Utils::ThreadTracker.new
12
21
  class << self
13
22
  def propagate propagation_node, preshift, target
23
+ return Contrast::Agent::Assess::Policy::Propagator::Keep.propagate(propagation_node, preshift, target) unless target.is_a?(Array)
24
+
14
25
  source = find_source(propagation_node.sources[0], preshift)
15
26
 
16
27
  separator_length = if propagation_node.method_name == :grapheme_clusters
@@ -22,7 +33,6 @@ module Contrast
22
33
  end
23
34
 
24
35
  current_index = 0
25
-
26
36
  target.each do |elem|
27
37
  elem_length = elem.length
28
38
  range = Contrast::Agent::Assess::AdjustedSpan.new(current_index, current_index + elem_length)
@@ -40,6 +50,132 @@ module Contrast
40
50
  current_index = current_index + elem_length + separator_length
41
51
  end
42
52
  end
53
+
54
+ # Marks the point in which the String#split method is called.
55
+ # Responsible for building the context required to propagate when
56
+ # the results of #split are yielded directly to a block
57
+ #
58
+ # @param string [String] the String on which split is invoked
59
+ # @param args [Array<Object>] the arguments passed to the
60
+ # original split call
61
+ def begin_split string, args
62
+ save_split_depth!
63
+ depth = SPLIT_TRACKER.get(:split_depth)
64
+ save_split_index!(depth)
65
+ save_split_value!(depth, string, args)
66
+ rescue Exception => e # rubocop:disable Lint/RescueException
67
+ # don't let our errors propagate and disable String#split for
68
+ # this since we're in an error state
69
+ logger.warn(e, 'Unable to record split context')
70
+ end_split
71
+ end
72
+
73
+ # Marks the point in which the String#split method is exited.
74
+ # Responsible for removing the context required to propagate when
75
+ # the results of #split are yielded directly to a block
76
+ def end_split
77
+ depth = SPLIT_TRACKER.get(:split_depth)
78
+ return unless depth
79
+
80
+ depth -= 1
81
+ if depth.negative?
82
+ SPLIT_TRACKER.delete(:split_depth)
83
+ SPLIT_TRACKER.delete(:split_index)
84
+ SPLIT_TRACKER.delete(:split_value)
85
+ else
86
+ SPLIT_TRACKER.set(:split_depth, depth)
87
+ end
88
+ rescue StandardError => e
89
+ logger.warn(e, 'Unable to remove split context')
90
+ end
91
+
92
+ # This method is called whenever an rb_yield is called. We need
93
+ # to leave it as soon as possible with as little work as
94
+ # possible.
95
+ #
96
+ # @param target [String] the entity being passed to the yield
97
+ # block
98
+ def propagate_yield target
99
+ depth = SPLIT_TRACKER.get(:split_depth)
100
+ return unless depth
101
+
102
+ source = SPLIT_TRACKER.get(:split_value)&.fetch(depth)
103
+ return unless source
104
+
105
+ index = SPLIT_TRACKER.get(:split_index)&.fetch(depth)
106
+ return unless index
107
+
108
+ true_source = source[index]
109
+ target.cs__copy_from(true_source)
110
+ rescue StandardError => e
111
+ logger.warn(e, 'Unable to track within split context')
112
+ ensure
113
+ if defined?(depth) && defined?(index) && depth && index
114
+ idx = SPLIT_TRACKER.get(:split_index)
115
+ idx[depth] = index + 1 if defined?(idx) && idx.is_a?(Array)
116
+ end
117
+ end
118
+
119
+ def instrument_string_split
120
+ if @_instrument_string_split.nil?
121
+ @_instrument_string_split = begin
122
+ cs__scoped_require 'cs__assess_yield_track/cs__assess_yield_track' if AGENT.patch_yield?
123
+ true
124
+ rescue StandardError => e
125
+ logger.error(e, 'Error loading split rb_yield patch')
126
+ false
127
+ end
128
+ end
129
+ @_instrument_string_split
130
+ end
131
+
132
+ private
133
+
134
+ def save_split_depth!
135
+ depth = SPLIT_TRACKER.get(:split_depth)
136
+ if depth
137
+ depth += 1
138
+ SPLIT_TRACKER.set(:split_depth, depth)
139
+ else
140
+ SPLIT_TRACKER.set(:split_depth, 0)
141
+ end
142
+ end
143
+
144
+ def save_split_index! depth
145
+ split_index = SPLIT_TRACKER.get(:split_index)
146
+ unless split_index
147
+ split_index = []
148
+ SPLIT_TRACKER.set(:split_index, split_index)
149
+ end
150
+ # save the index to the ThreadLocal; not useless
151
+ split_index[depth] = 0 # rubocop:disable Lint/UselessSetterCall
152
+ end
153
+
154
+ def save_split_value! depth, string, args
155
+ preshift = Contrast::Agent::Assess::PreShift.build_preshift(split_node, string, args)
156
+ target = string.split
157
+ propagate(split_node, preshift, target)
158
+ split_value = SPLIT_TRACKER.get(:split_value)
159
+ unless split_value
160
+ split_value = []
161
+ SPLIT_TRACKER.set(:split_value, split_value)
162
+ end
163
+ # save the target to the ThreadLocal; not useless
164
+ split_value[depth] = target # rubocop:disable Lint/UselessSetterCall
165
+ end
166
+
167
+ # Quick hook to the String#split propagation node in our Assess
168
+ # policy
169
+ #
170
+ # @return [Contrast::Agent::Assess::Policy::PropagationNode]
171
+ # String#split node
172
+ def split_node
173
+ @_split_node ||= begin
174
+ Contrast::Agent::Assess::Policy::Policy.instance.propagators.find do |node|
175
+ node.class_name == 'String' && node.method_name == :split && node.instance_method?
176
+ end
177
+ end
178
+ end
43
179
  end
44
180
  end
45
181
  end
@@ -47,3 +183,32 @@ module Contrast
47
183
  end
48
184
  end
49
185
  end
186
+
187
+ if RUBY_VERSION >= '2.6.0'
188
+ # Special class to handle String#split in 2.6 which, when given a block,
189
+ # propagates each split piece directly
190
+ class String
191
+ alias_method :cs__patched_string_split_special, :split
192
+
193
+ # override of the the standard split method to handle the 2.6 direct
194
+ # yield case.
195
+ #
196
+ # Note: because this patch is applied before our standard propagation, this
197
+ # call wrapped in it. As such, any call here happens in scope, so there is
198
+ # no need to manage it on our own.
199
+ def split *args, &block
200
+ if block
201
+ Contrast::Agent::Assess::Policy::Propagator::Split.begin_split(self, args)
202
+ begin
203
+ cs__patched_string_split_special(*args, &block)
204
+ ensure
205
+ Contrast::Agent::Assess::Policy::Propagator::Split.end_split
206
+ end
207
+ else
208
+ cs__patched_string_split_special(*args, &block)
209
+ end
210
+ end
211
+ end
212
+
213
+ Contrast::Agent::Assess::Policy::Propagator::Split.instrument_string_split
214
+ end
@@ -47,6 +47,7 @@ module Contrast
47
47
  return unless ASSESS.enabled?
48
48
  return unless AGENT.rewrite_interpolation?
49
49
  return unless AGENT.interpolation_enabled?
50
+ return if AGENT.skip_instrumentation? mod.cs__name
50
51
  return if mod.cs__frozen?
51
52
  return if mod.singleton_class?
52
53
  return if mid_defining?(mod)
@@ -13,6 +13,7 @@ cs__scoped_require 'set'
13
13
  cs__scoped_require 'contrast/utils/object_share'
14
14
  cs__scoped_require 'contrast/utils/sha256_builder'
15
15
  cs__scoped_require 'contrast/agent/assess/adjusted_span'
16
+ cs__scoped_require 'contrast/agent/assess/policy/source_validation/source_validation'
16
17
 
17
18
  cs__scoped_require 'contrast/components/interface'
18
19
 
@@ -28,109 +29,149 @@ module Contrast
28
29
  include Contrast::Components::Interface
29
30
  access_component :logging, :analysis
30
31
 
31
- def self.determine_target source_node, object, ret, args
32
- source_target = source_node.targets[0]
33
- case source_target
34
- when Contrast::Utils::ObjectShare::RETURN_KEY
35
- ret
36
- when Contrast::Utils::ObjectShare::OBJECT_KEY
37
- object
38
- else
39
- if source_target.is_a?(Integer)
40
- args[source_target]
41
- # If this isn't an index param, it's a named one. R.I.P.
42
- else
43
- arg = nil
44
- args.each do |search|
45
- next unless search.is_a?(Hash)
32
+ PARAMETER_TYPE = 'PARAMETER'
33
+ PARAMETER_KEY_TYPE = 'PARAMETER_KEY'
34
+ HEADER_TYPE = 'HEADER'
35
+ HEADER_KEY_TYPE = 'HEADER_KEY'
36
+ COOKIE_TYPE = 'COOKIE'
37
+ COOKIE_KEY_TYPE = 'COOKIE_KEY'
46
38
 
47
- arg = search[source_target]
48
- break if arg
49
- end
50
- arg
39
+ class << self
40
+ # This is called from within our woven proc. It will be called as if it
41
+ # were inline in the Rack application.
42
+ #
43
+ # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
44
+ # the policy that applies to the method being called
45
+ # @param object [Object] the Object on which the method was invoked
46
+ # @param ret [Object] the Return of the invoked method
47
+ # @param args [Array<Object>] the Arguments with which the method
48
+ # was invoked
49
+ # @return [Object, nil] the tracked Return or nil if no changes
50
+ # were made
51
+ def source_patchers method_policy, object, ret, args
52
+ return if method_policy.source_node.nil?
53
+
54
+ current_context = Contrast::Agent::REQUEST_TRACKER.current
55
+ return unless current_context&.analyze_request? && ASSESS.enabled?
56
+
57
+ replaced_return = nil
58
+ source_node = method_policy.source_node
59
+
60
+ target = determine_target(source_node, object, ret, args)
61
+
62
+ # We don't propagate to frozen things that haven't been tracked
63
+ # before. By definition, something that is a source has not
64
+ # previously been tracked; therefore, we can break out early.
65
+ if target.cs__frozen?
66
+ # That being said, we don't have enough context to know if we
67
+ # can make this assumption and still function, so we'll allow for
68
+ # source tracking of frozen things by a common config setting.
69
+ #
70
+ # Rails' StrongParameters make a case for this to be default
71
+ # behavior
72
+ return replaced_return unless ASSESS.track_frozen_sources?
73
+
74
+ # If we're tracking the frozen target, we need to unfreeze
75
+ # (dup) it to track and then freeze that result. For
76
+ # simplicities sake, we ONLY do this if the return is the
77
+ # target (I don't want to have to deal with unfreezing self)
78
+ return replaced_return unless source_node.targets[0] == Contrast::Utils::ObjectShare::RETURN_KEY
79
+
80
+ restore_frozen_state = true
81
+ ret = Contrast::Utils::FreezeUtil.unfreeze_dup(ret)
82
+ target = ret
51
83
  end
52
- end
53
- end
54
84
 
55
- # This is called from within our woven proc. It will be called as if it
56
- # were inline in the Rack application.
57
- def self.source_patchers method_policy, object, ret, args
58
- return if method_policy.source_node.nil?
59
-
60
- current_context = Contrast::Agent::REQUEST_TRACKER.current
61
- return unless current_context&.analyze_request? && ASSESS.enabled?
62
-
63
- replaced_return = nil
64
- source_node = method_policy.source_node
65
-
66
- target = determine_target(source_node, object, ret, args)
67
-
68
- # We don't propagate to frozen things that haven't been tracked
69
- # before. By definition, something that is a source has not
70
- # previously been tracked; therefore, we can break out early.
71
- if target.cs__frozen?
72
- # That being said, we don't have enough context to know if we
73
- # can make this assumption and still function, so we'll allow for
74
- # source tracking of frozen things by a common config setting.
75
- #
76
- # Rails' StrongParameters make a case for this to be default
77
- # behavior
78
- return replaced_return unless ASSESS.track_frozen_sources?
79
-
80
- # If we're tracking the frozen target, we need to unfreeze
81
- # (dup) it to track and then freeze that result. For
82
- # simplicities sake, we ONLY do this if the return is the
83
- # target (I don't want to have to deal with unfreezing self)
84
- return replaced_return unless source_node.targets[0] == Contrast::Utils::ObjectShare::RETURN_KEY
85
-
86
- restore_frozen_state = true
87
- ret = Contrast::Utils::FreezeUtil.unfreeze_dup(ret)
88
- target = ret
89
- end
85
+ invoked = 3 # apply_post_patch => apply_assess => source_patchers
86
+ apply_source(current_context, source_node, target, object, ret, source_node.type, nil, invoked, *args)
90
87
 
91
- SourceMethod.cs__apply_source(current_context, source_node, target, object, ret, *args)
88
+ ret.cs__freeze if restore_frozen_state
89
+ ret
90
+ end
92
91
 
93
- ret.cs__freeze if restore_frozen_state
94
- ret
95
- end
92
+ private
96
93
 
97
- # This is our method that actually taints the object our source_node
98
- # targets.
99
- def self.cs__apply_source context, source_node, target, object, ret, *args
100
- return unless context
101
-
102
- source_node_source = source_node.sources[0]
103
- source_name = case source_node_source
104
- when nil
105
- nil
106
- when Contrast::Utils::ObjectShare::RETURN_KEY
107
- ret
108
- when Contrast::Utils::ObjectShare::OBJECT_KEY
109
- self
110
- else
111
- args[source_node_source]
112
- end
113
- _cs__apply_source context, source_node, target, object, ret, source_node.type, source_name, 0, *args
114
- end
94
+ # This is our method that actually taints the object our
95
+ # source_node targets.
96
+ #
97
+ # @param context [Contrast::Utils::ThreadTracker] the current request
98
+ # context
99
+ # @param source_node [Contrast::Agent::Assess::Policy::SourceNode]
100
+ # the node to direct applying this source event
101
+ # @param target [Object] the target of the Source Event
102
+ # @param object [Object] the Object on which the method was invoked
103
+ # @param ret [Object] the Return of the invoked method
104
+ # @param source_type [String] the type of this source, from the
105
+ # source_node, or a KEY_TYPE if invoked for a map
106
+ # @param source_name [String, nil] the name of this source, i.e.
107
+ # the key used to accessed if from a map or nil if a type like
108
+ # BODY
109
+ # @param invoked [Integer] the depth of this invocation from
110
+ # application code; often a lie.
111
+ # @param args [Array<Object>] the Arguments with which the method
112
+ # was invoked
113
+ def apply_source context, source_node, target, object, ret, source_type, source_name = nil, invoked = 0, *args
114
+ return unless context && source_node && target
115
115
 
116
- # I lied above. We had to figure out what the target of the source was.
117
- # Now that we know, we'll actually tag it.
118
- def self._cs__apply_source context, source_node, target, object, ret, source_type, source_name = nil, invoked = 0, *args
119
- return unless context && source_node && target
116
+ source_name ||= determine_source_name(source_node, object, ret, *args)
117
+ # We know we only work on certain things.
118
+ # Skip if this isn't one of them
119
+ if Contrast::Utils::DuckUtils.quacks_to?(target, :cs__properties)
120
+ apply_tags(source_node, target, object, ret, source_type, source_name, invoked, *args)
121
+ # While we don't taint hashes themselves, we may taint the things
122
+ # they hold. Let's pass their keys and values back to ourselves and
123
+ # try again
124
+ elsif Contrast::Utils::DuckUtils.iterable_hash?(target)
125
+ source_key_type = invoked.zero? ? key_type(source_type) : source_type
126
+ invoked += 1
127
+ to_replace = []
128
+ target.each_pair do |key, value|
129
+ # We only do this for Strings b/c of the way Hash lookup works.
130
+ # To replace another object would break hash lookup and,
131
+ # therefore, the application
132
+ if ASSESS.track_frozen_sources? &&
133
+ key.is_a?(String) &&
134
+ Contrast::Utils::DuckUtils.quacks_to?(target, :delete)
135
+ key = Contrast::Utils::FreezeUtil.unfreeze_dup(key)
136
+ to_replace << key
137
+ end
138
+ apply_source(context, source_node, key, object, ret, source_key_type, key, invoked, *args)
139
+ apply_source(context, source_node, value, object, ret, source_type, key, invoked, *args)
140
+ end
120
141
 
121
- # We know we only work on certain things.
122
- # Skip if this isn't one of them
123
- if Contrast::Utils::DuckUtils.quacks_to?(target, :cs__properties)
142
+ # Hash is designed to keep one instance of the string key in it.
143
+ # We need to remove the existing one and replace it with our new
144
+ # tracked one.
145
+ to_replace.each do |key|
146
+ key.cs__freeze
147
+ value = target[key]
148
+ target.delete(key)
149
+ target[key] = value
150
+ end
151
+ # While we don't taint arrays themselves, we may taint the things
152
+ # they hold. Let's pass their keys and values back to ourselves and
153
+ # try again
154
+ elsif Contrast::Utils::DuckUtils.iterable_enumerable?(target)
155
+ invoked += 1
156
+ target.each { |value| apply_source(context, source_node, value, object, ret, source_type, source_name, invoked, *args) }
157
+ end
158
+ rescue StandardError => e
159
+ logger.warn(e, "Unable to apply source for source_node #{ source_node.id }")
160
+ end
124
161
 
162
+ def apply_tags source_node, target, object, ret, source_type, source_name, invoked, *args
125
163
  # don't apply second source -- probably needs tuning later if we
126
164
  # use more than 'UNTRUSTED' in our sources
127
165
  return if target.cs__tracked? || target.cs__frozen?
128
166
 
167
+ invoked += 1
129
168
  # otherwise for each tag this source_node applies, create a tag range
130
169
  # on the target object
131
170
  # I realize this looping is counter-intuitive from the above
132
171
  # message, that's why we're revisiting.
133
172
  source_node.tags.each do |tag|
173
+ next unless Contrast::Agent::Assess::Policy::SourceValidation.valid?(tag, source_type, source_name)
174
+
134
175
  length = Contrast::Utils::StringUtils.ret_length(target)
135
176
  target.cs__properties.add_tag(tag, Contrast::Agent::Assess::AdjustedSpan.new(0, length))
136
177
  target.cs__properties.add_properties(source_node.properties)
@@ -139,67 +180,85 @@ module Contrast
139
180
 
140
181
  # make a representation of this method that TeamServer can render
141
182
  target.cs__properties.build_event(source_node, target, object, ret, args, invoked, source_type, source_name)
183
+ end
142
184
 
143
- # While we don't taint hashes themselves, we may taint the things
144
- # they hold. Let's pass their keys and values back to ourselves and
145
- # try again
146
- elsif Contrast::Utils::DuckUtils.quacks_like_tracked_hash?(target)
147
- source_key_type = invoked.zero? ? key_type(source_type) : source_type
148
- invoked += 1
149
- to_replace = []
150
- target.each_pair do |key, value|
151
- # We only do this for Strings b/c of the way Hash lookup works.
152
- # To replace another object would break hash lookup and,
153
- # therefore, the application
154
- if ASSESS.track_frozen_sources? &&
155
- key.is_a?(String) &&
156
- Contrast::Utils::DuckUtils.quacks_to?(target, :delete)
157
- key = Contrast::Utils::FreezeUtil.unfreeze_dup(key)
158
- to_replace << key
159
- end
160
- _cs__apply_source(context, source_node, key, object, ret, source_key_type, key, invoked, *args)
161
- _cs__apply_source(context, source_node, value, object, ret, source_type, key, invoked, *args)
162
- end
185
+ # Find the name of the source
186
+ #
187
+ # @param source_node [Contrast::Agent::Assess::Policy::SourceNode]
188
+ # the node to direct applying this source event
189
+ # @param object [Object] the Object on which the method was invoked
190
+ # @param ret [Object] the Return of the invoked method
191
+ # @param args [Array<Object>] the Arguments with which the method
192
+ # was invoked
193
+ # @return [String, nil] the human readable name of the target to
194
+ # which this source event applies, or nil if none provided by the
195
+ # node
196
+ def determine_source_name source_node, object, ret, *args
197
+ return source_node.get_property('dynamic_source_name') if source_node.type == 'UNTRUSTED_DATABASE'
163
198
 
164
- # Hash is designed to keep one instance of the string key in it.
165
- # We need to remove the existing one and replace it with our new
166
- # tracked one.
167
- to_replace.each do |key|
168
- key.cs__freeze
169
- value = target[key]
170
- target.delete(key)
171
- target[key] = value
199
+ source_node_source = source_node.sources[0]
200
+ case source_node_source
201
+ when nil
202
+ nil
203
+ when Contrast::Utils::ObjectShare::RETURN_KEY
204
+ ret
205
+ when Contrast::Utils::ObjectShare::OBJECT_KEY
206
+ object
207
+ else
208
+ args[source_node_source]
172
209
  end
173
- # While we don't taint arrays themselves, we may taint the things
174
- # they hold. Let's pass their keys and values back to ourselves and
175
- # try again
176
- elsif Contrast::Utils::DuckUtils.quacks_like_tracked_enumerable?(target)
177
- invoked += 1
178
- target.each { |value| _cs__apply_source(context, source_node, value, object, ret, source_type, source_name, invoked, *args) }
179
210
  end
180
- rescue StandardError => e
181
- logger.warn(e, "Unable to apply source for source_node #{ source_node.id }")
182
- end
183
211
 
184
- # Silly helper method so that TeamServer can properly mark up
185
- # the source of this trace, if this source ends up in a trigger
186
- PARAMETER_TYPE = 'PARAMETER'
187
- PARAMETER_KEY_TYPE = 'PARAMETER_KEY'
188
- HEADER_TYPE = 'HEADER'
189
- HEADER_KEY_TYPE = 'HEADER_KEY'
190
- COOKIE_TYPE = 'COOKIE'
191
- COOKIE_KEY_TYPE = 'COOKIE_KEY'
212
+ # Find the literal target of the propagation
213
+ #
214
+ # @param source_node [Contrast::Agent::Assess::Policy::SourceNode]
215
+ # the node to direct applying this source event
216
+ # @param object [Object] the Object on which the method was invoked
217
+ # @param ret [Object] the Return of the invoked method
218
+ # @param args [Array<Object>] the Arguments with which the method
219
+ # was invoked
220
+ # @return [Object] the target to which this source event applies
221
+ def determine_target source_node, object, ret, args
222
+ source_target = source_node.targets[0]
223
+ case source_target
224
+ when Contrast::Utils::ObjectShare::RETURN_KEY
225
+ ret
226
+ when Contrast::Utils::ObjectShare::OBJECT_KEY
227
+ object
228
+ else
229
+ if source_target.is_a?(Integer)
230
+ args[source_target]
231
+ # If this isn't an index param, it's a named one. R.I.P.
232
+ else
233
+ arg = nil
234
+ args.each do |search|
235
+ next unless search.is_a?(Hash)
192
236
 
193
- def self.key_type source_type
194
- case source_type
195
- when PARAMETER_TYPE
196
- PARAMETER_KEY_TYPE
197
- when HEADER_TYPE
198
- HEADER_KEY_TYPE
199
- when COOKIE_TYPE
200
- COOKIE_KEY_TYPE
201
- else
202
- source_type
237
+ arg = search[source_target]
238
+ break if arg
239
+ end
240
+ arg
241
+ end
242
+ end
243
+ end
244
+
245
+ # Simple helper method to flip the type from value to key when the
246
+ # source is the key of a Hash
247
+ #
248
+ # @param source_type [String] the original value source type
249
+ # @return [String] the key form of the source type, if one exists,
250
+ # else the original source type
251
+ def key_type source_type
252
+ case source_type
253
+ when PARAMETER_TYPE
254
+ PARAMETER_KEY_TYPE
255
+ when HEADER_TYPE
256
+ HEADER_KEY_TYPE
257
+ when COOKIE_TYPE
258
+ COOKIE_KEY_TYPE
259
+ else
260
+ source_type
261
+ end
203
262
  end
204
263
  end
205
264
  end