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.
- checksums.yaml +4 -4
- data/ext/cs__assess_array/cs__assess_array.c +1 -1
- data/ext/cs__assess_module/cs__assess_module.c +0 -1
- data/ext/cs__assess_yield_track/cs__assess_yield_track.c +34 -0
- data/ext/cs__assess_yield_track/cs__assess_yield_track.h +12 -0
- data/ext/{cs__scope → cs__assess_yield_track}/extconf.rb +0 -0
- data/ext/cs__common/cs__common.c +6 -6
- data/ext/cs__common/cs__common.h +3 -1
- data/ext/cs__contrast_patch/cs__contrast_patch.c +142 -119
- data/ext/cs__contrast_patch/cs__contrast_patch.h +3 -0
- data/funchook/autom4te.cache/requests +48 -48
- data/funchook/config.log +2 -2
- data/lib/contrast/agent.rb +15 -5
- data/lib/contrast/agent/assess.rb +0 -1
- data/lib/contrast/agent/assess/contrast_event.rb +9 -8
- data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +68 -18
- data/lib/contrast/agent/assess/policy/policy.rb +0 -14
- data/lib/contrast/agent/assess/policy/policy_scanner.rb +1 -1
- data/lib/contrast/agent/assess/policy/preshift.rb +1 -1
- data/lib/contrast/agent/assess/policy/propagation_method.rb +4 -2
- data/lib/contrast/agent/assess/policy/propagator/custom.rb +1 -1
- data/lib/contrast/agent/assess/policy/propagator/database_write.rb +1 -1
- data/lib/contrast/agent/assess/policy/propagator/splat.rb +2 -2
- data/lib/contrast/agent/assess/policy/propagator/split.rb +166 -1
- data/lib/contrast/agent/assess/policy/rewriter_patch.rb +1 -0
- data/lib/contrast/agent/assess/policy/source_method.rb +199 -140
- data/lib/contrast/agent/assess/policy/source_validation/cross_site_validator.rb +30 -0
- data/lib/contrast/agent/assess/policy/source_validation/source_validation.rb +36 -0
- data/lib/contrast/agent/assess/policy/trigger_method.rb +238 -153
- data/lib/contrast/agent/assess/policy/trigger_node.rb +54 -9
- data/lib/contrast/agent/assess/policy/trigger_validation/trigger_validation.rb +13 -0
- data/lib/contrast/agent/assess/properties.rb +29 -0
- data/lib/contrast/agent/assess/rule/csrf/csrf_applicator.rb +35 -31
- data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +1 -1
- data/lib/contrast/agent/class_reopener.rb +98 -55
- data/lib/contrast/agent/feature_state.rb +1 -1
- data/lib/contrast/agent/inventory/policy/policy.rb +1 -1
- data/lib/contrast/agent/logger_manager.rb +2 -2
- data/lib/contrast/agent/middleware.rb +1 -3
- data/lib/contrast/agent/patching/policy/after_load_patch.rb +40 -4
- data/lib/contrast/agent/patching/policy/after_load_patcher.rb +33 -8
- data/lib/contrast/agent/patching/policy/method_policy.rb +20 -7
- data/lib/contrast/agent/patching/policy/patch.rb +54 -23
- data/lib/contrast/agent/patching/policy/patch_status.rb +0 -2
- data/lib/contrast/agent/patching/policy/patcher.rb +10 -11
- data/lib/contrast/agent/patching/policy/policy.rb +4 -0
- data/lib/contrast/agent/patching/policy/policy_node.rb +14 -1
- data/lib/contrast/agent/patching/policy/trigger_node.rb +2 -1
- data/lib/contrast/agent/protect/policy/policy.rb +6 -6
- data/lib/contrast/agent/protect/rule/base.rb +1 -1
- data/lib/contrast/agent/protect/rule/deserialization.rb +3 -25
- data/lib/contrast/agent/protect/rule/sqli.rb +1 -1
- data/lib/contrast/agent/railtie.rb +11 -5
- data/lib/contrast/agent/request.rb +1 -19
- data/lib/contrast/agent/request_context.rb +1 -1
- data/lib/contrast/agent/rewriter.rb +4 -3
- data/lib/contrast/agent/scope.rb +116 -19
- data/lib/contrast/agent/service_heartbeat.rb +5 -2
- data/lib/contrast/agent/settings_state.rb +12 -8
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/api.rb +1 -0
- data/lib/contrast/api/speedracer.rb +2 -2
- data/lib/contrast/components/agent.rb +26 -7
- data/lib/contrast/components/app_context.rb +8 -45
- data/lib/contrast/components/contrast_service.rb +3 -4
- data/lib/contrast/components/interface.rb +1 -1
- data/lib/contrast/components/scope.rb +56 -26
- data/lib/contrast/config/ruby_configuration.rb +8 -3
- data/lib/contrast/delegators.rb +9 -0
- data/lib/contrast/delegators/application_update.rb +32 -0
- data/lib/contrast/extensions/framework/rack/cookie.rb +24 -0
- data/lib/contrast/extensions/framework/rack/request.rb +24 -0
- data/lib/contrast/extensions/framework/rack/response.rb +23 -0
- data/lib/contrast/extensions/framework/rails/action_controller_railties_helper_inherited.rb +20 -0
- data/lib/contrast/extensions/framework/rails/active_record.rb +26 -0
- data/lib/contrast/extensions/framework/rails/active_record_named.rb +53 -0
- data/lib/contrast/extensions/framework/rails/active_record_time_zone_inherited.rb +21 -0
- data/lib/contrast/extensions/framework/rails/buffer.rb +28 -0
- data/lib/contrast/extensions/framework/rails/configuration.rb +27 -0
- data/lib/contrast/extensions/framework/sinatra/base.rb +59 -0
- data/lib/contrast/{core_extensions → extensions/ruby_core}/assess.rb +12 -11
- data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/array.rb +4 -3
- data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/assess_extension.rb +0 -2
- data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/basic_object.rb +1 -1
- data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/erb.rb +0 -0
- data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/exec_trigger.rb +0 -0
- data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/fiber.rb +3 -4
- data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/hash.rb +0 -0
- data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/kernel.rb +1 -1
- data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/module.rb +1 -1
- data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/regexp.rb +0 -0
- data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/string.rb +0 -0
- data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/tilt_template_trigger.rb +0 -0
- data/lib/contrast/extensions/ruby_core/assess/xpath_library_trigger.rb +40 -0
- data/lib/contrast/{core_extensions → extensions/ruby_core}/delegator.rb +0 -0
- data/lib/contrast/{core_extensions → extensions/ruby_core}/eval_trigger.rb +1 -1
- data/lib/contrast/{core_extensions → extensions/ruby_core}/inventory.rb +0 -0
- data/lib/contrast/{core_extensions → extensions/ruby_core}/inventory/datastores.rb +1 -1
- data/lib/contrast/extensions/ruby_core/module.rb +17 -0
- data/lib/contrast/{core_extensions → extensions/ruby_core}/protect.rb +0 -0
- data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_command_injection_rule.rb +8 -6
- data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_deserialization_rule.rb +7 -5
- data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_no_sqli_rule.rb +5 -3
- data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_path_traversal_rule.rb +31 -27
- data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_sqli_rule.rb +5 -3
- data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_xxe_rule.rb +9 -7
- data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/kernel.rb +0 -0
- data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/psych.rb +1 -1
- data/lib/contrast/{core_extensions → extensions/ruby_core}/thread.rb +0 -0
- data/lib/contrast/framework/base_support.rb +63 -0
- data/lib/contrast/framework/manager.rb +109 -0
- data/lib/contrast/framework/platform_version.rb +21 -0
- data/lib/contrast/framework/rails_support.rb +88 -0
- data/lib/contrast/framework/sinatra_application_helper.rb +49 -0
- data/lib/contrast/framework/sinatra_support.rb +94 -0
- data/lib/contrast/framework/view_technologies_descriptor.rb +20 -0
- data/lib/contrast/utils/assess/tracking_util.rb +2 -4
- data/lib/contrast/utils/class_util.rb +92 -37
- data/lib/contrast/utils/duck_utils.rb +59 -39
- data/lib/contrast/utils/environment_util.rb +5 -75
- data/lib/contrast/utils/freeze_util.rb +3 -7
- data/lib/contrast/utils/invalid_configuration_util.rb +5 -5
- data/lib/contrast/utils/job_servers_running.rb +39 -0
- data/lib/contrast/utils/ruby_ast_rewriter.rb +2 -2
- data/lib/contrast/utils/service_response_util.rb +0 -6
- data/lib/contrast/utils/sinatra_helper.rb +6 -0
- data/lib/contrast/utils/stack_trace_utils.rb +1 -1
- data/resources/assess/policy.json +74 -23
- data/resources/inventory/policy.json +1 -1
- data/resources/protect/policy.json +11 -9
- data/resources/rubocops/object/frozen_cop.rb +1 -1
- data/ruby-agent.gemspec +2 -0
- data/service_executables/VERSION +1 -1
- data/service_executables/linux/contrast-service +0 -0
- data/service_executables/mac/contrast-service +0 -0
- metadata +94 -57
- data/ext/cs__scope/cs__scope.c +0 -96
- data/ext/cs__scope/cs__scope.h +0 -33
- data/lib/contrast/agent/assess/class_reverter.rb +0 -82
- data/lib/contrast/agent/patching/policy/policy_unpatcher.rb +0 -28
- data/lib/contrast/core_extensions/module.rb +0 -42
- data/lib/contrast/core_extensions/object.rb +0 -27
- data/lib/contrast/rails_extensions/assess/action_controller_inheritance.rb +0 -48
- data/lib/contrast/rails_extensions/assess/active_record.rb +0 -32
- data/lib/contrast/rails_extensions/assess/active_record_named.rb +0 -61
- data/lib/contrast/rails_extensions/assess/configuration.rb +0 -26
- data/lib/contrast/rails_extensions/buffer.rb +0 -30
- data/lib/contrast/rails_extensions/rack.rb +0 -45
- data/lib/contrast/sinatra_extensions/assess/cookie.rb +0 -26
- data/lib/contrast/sinatra_extensions/inventory/sinatra_base.rb +0 -59
- data/lib/contrast/utils/operating_environment.rb +0 -38
- data/lib/contrast/utils/path_util.rb +0 -151
- 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.
|
|
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.
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
88
|
+
ret.cs__freeze if restore_frozen_state
|
|
89
|
+
ret
|
|
90
|
+
end
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
ret
|
|
95
|
-
end
|
|
92
|
+
private
|
|
96
93
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|