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
|
@@ -101,13 +101,13 @@ module Contrast
|
|
|
101
101
|
return false unless source.cs__tracked?
|
|
102
102
|
|
|
103
103
|
# find the ranges that violate the rule (untrusted, etc)
|
|
104
|
-
vulnerable_ranges =
|
|
104
|
+
vulnerable_ranges = find_ranges_by_all_tags(Contrast::Utils::StringUtils.ret_length(source), source.cs__properties, required_tags)
|
|
105
105
|
# if there aren't any vulnerable ranges, nope out
|
|
106
106
|
return false if vulnerable_ranges.empty?
|
|
107
107
|
|
|
108
108
|
# find the ranges that are exempt from the rule
|
|
109
109
|
# (validated, sanitized, etc)
|
|
110
|
-
secure_ranges =
|
|
110
|
+
secure_ranges = find_ranges_by_any_tag(source.cs__properties, disallowed_tags)
|
|
111
111
|
# if there are vulnerable ranges and no secure, report
|
|
112
112
|
return true if secure_ranges.empty?
|
|
113
113
|
|
|
@@ -135,7 +135,7 @@ module Contrast
|
|
|
135
135
|
return unless dataflow?
|
|
136
136
|
|
|
137
137
|
validate_rule_tags(required_tags)
|
|
138
|
-
@required_tags = required_tags
|
|
138
|
+
@required_tags = Set.new(required_tags)
|
|
139
139
|
@required_tags << UNTRUSTED
|
|
140
140
|
end
|
|
141
141
|
|
|
@@ -153,7 +153,7 @@ module Contrast
|
|
|
153
153
|
return unless dataflow?
|
|
154
154
|
|
|
155
155
|
validate_rule_tags(disallowed_tags)
|
|
156
|
-
@disallowed_tags = disallowed_tags
|
|
156
|
+
@disallowed_tags = Set.new(disallowed_tags)
|
|
157
157
|
@disallowed_tags << LIMITED_CHARS
|
|
158
158
|
@disallowed_tags << CUSTOM_ENCODED
|
|
159
159
|
@disallowed_tags << CUSTOM_VALIDATED
|
|
@@ -172,14 +172,60 @@ module Contrast
|
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
# Find the ranges that satisfy all of the given tags.
|
|
176
|
+
#
|
|
177
|
+
# @param length [Integer] the length of the object which may have the
|
|
178
|
+
# given tags -- used as the maximum index to search for all of the
|
|
179
|
+
# tags.
|
|
180
|
+
# @param cs__properties [Contrast::Agent::Assess::Properties] the
|
|
181
|
+
# properties to check for the tags
|
|
182
|
+
# @param tags [Set<String>] the list of tags on which to match
|
|
183
|
+
# @return [Array<Contrast::Agent::Assess::Tag>] the ranges satisfied
|
|
184
|
+
# by the given conditions
|
|
185
|
+
def find_ranges_by_all_tags length, cs__properties, tags
|
|
186
|
+
# if there aren't any all_tags or tags, break early
|
|
187
|
+
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless cs__properties.tracked?
|
|
188
|
+
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tags&.any?
|
|
189
|
+
|
|
190
|
+
# :zap: faster to treat all as any if there's only one tag
|
|
191
|
+
return find_ranges_by_any_tag(cs__properties, tags) if tags.length == 1
|
|
192
|
+
|
|
176
193
|
ranges = []
|
|
194
|
+
# find all the indicies on the source that have all the given tags
|
|
195
|
+
(0..length).each do |idx|
|
|
196
|
+
tags_at = cs__properties.tag_names_at(idx)
|
|
197
|
+
ranges << idx if tags.all? { |tag| tags_at.include?(tag) }
|
|
198
|
+
end
|
|
199
|
+
# break early if no indicies satisfy all the tags
|
|
200
|
+
return Contrast::Utils::ObjectShare::EMPTY_ARRAY if ranges.empty?
|
|
201
|
+
|
|
202
|
+
# chunk all the adjacent ranges
|
|
203
|
+
chunked = ranges.chunk_while { |i, j| i + 1 == j }
|
|
204
|
+
tag_ranges = []
|
|
205
|
+
# and convert them into Tags
|
|
206
|
+
chunked.each do |join|
|
|
207
|
+
start = join[0]
|
|
208
|
+
stop = join[-1]
|
|
209
|
+
# add the 1 to account for end index being exclusive
|
|
210
|
+
tag_length = stop - start + 1
|
|
211
|
+
tag_ranges = Contrast::Utils::TagUtil.ordered_merge(tag_ranges, Tag.new(tag_length, start))
|
|
212
|
+
end
|
|
213
|
+
tag_ranges
|
|
214
|
+
end
|
|
177
215
|
|
|
216
|
+
# Find the ranges that satisfy any of the given tags.
|
|
217
|
+
#
|
|
218
|
+
# @param cs__properties [Contrast::Agent::Assess::Properties] the
|
|
219
|
+
# properties to check for the tags
|
|
220
|
+
# @param tags [Set<String>] the list of tags on which to match
|
|
221
|
+
# @return [Array<Contrast::Agent::Assess::Tag>] the ranges satisfied
|
|
222
|
+
# by the given conditions
|
|
223
|
+
def find_ranges_by_any_tag cs__properties, tags
|
|
178
224
|
# if there aren't any all_tags or tags, break early
|
|
179
|
-
return
|
|
180
|
-
return
|
|
225
|
+
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless cs__properties.tracked?
|
|
226
|
+
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tags&.any?
|
|
181
227
|
|
|
182
|
-
|
|
228
|
+
ranges = []
|
|
183
229
|
tags.each do |desired|
|
|
184
230
|
found = cs__properties.fetch_tag(desired)
|
|
185
231
|
next unless found
|
|
@@ -188,7 +234,6 @@ module Contrast
|
|
|
188
234
|
# used in another trace
|
|
189
235
|
ranges = Contrast::Utils::TagUtil.ordered_merge(ranges, found.dup)
|
|
190
236
|
end
|
|
191
|
-
|
|
192
237
|
ranges
|
|
193
238
|
end
|
|
194
239
|
end
|
|
@@ -18,6 +18,19 @@ module Contrast
|
|
|
18
18
|
Contrast::Agent::Assess::Policy::TriggerValidation::XSSValidator
|
|
19
19
|
].cs__freeze
|
|
20
20
|
|
|
21
|
+
# Determines if the conditions in which this trigger was called are
|
|
22
|
+
# valid and should result in the generation of a
|
|
23
|
+
# Contrast::Api::Dtm::Finding.
|
|
24
|
+
#
|
|
25
|
+
# @param patcher [Contrast::Agent::Assess::Policy::TriggerNode] the
|
|
26
|
+
# Node which applies to the Trigger Method
|
|
27
|
+
# @param object [Object] the Object on which the Trigger Method was
|
|
28
|
+
# invoked
|
|
29
|
+
# @param ret [Object] the return of the Trigger Method
|
|
30
|
+
# @param args [Array<Object>] the Arguments with which the Trigger
|
|
31
|
+
# Method was invoked
|
|
32
|
+
# @return [Boolean] if the conditions are valid for the generation of
|
|
33
|
+
# a Contrast::Api::Dtm::Finding
|
|
21
34
|
def self.valid? patcher, object, ret, args
|
|
22
35
|
VALIDATORS.each do |validator|
|
|
23
36
|
return false unless validator.valid?(patcher, object, ret, args)
|
|
@@ -66,6 +66,10 @@ module Contrast
|
|
|
66
66
|
# to be expanded out to the size of the new String.
|
|
67
67
|
#
|
|
68
68
|
# Note: Tags do not know their key, so this is only the range covered
|
|
69
|
+
#
|
|
70
|
+
# @param idx [Integer] the index to check for tags
|
|
71
|
+
# @return [Array<Contrast::Agent::Assess::Tag>] a set of all the Tags
|
|
72
|
+
# covering the given index. This is only the ranges, not the names.
|
|
69
73
|
def tags_at idx
|
|
70
74
|
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tags?
|
|
71
75
|
|
|
@@ -82,6 +86,27 @@ module Contrast
|
|
|
82
86
|
at
|
|
83
87
|
end
|
|
84
88
|
|
|
89
|
+
# Find all of the tag names that span a given index.
|
|
90
|
+
#
|
|
91
|
+
# @param idx [Integer] the index to check for tags
|
|
92
|
+
# @return [Set<String>] a set of all the tags covering the given index.
|
|
93
|
+
# This is only the names of the tags, not their ranges.
|
|
94
|
+
def tag_names_at idx
|
|
95
|
+
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tags?
|
|
96
|
+
|
|
97
|
+
at = Set.new
|
|
98
|
+
tags.each_pair do |tag_name, tag_array|
|
|
99
|
+
tag_array.each do |tag|
|
|
100
|
+
if tag.covers?(idx)
|
|
101
|
+
at << tag_name
|
|
102
|
+
elsif tag.above?(idx)
|
|
103
|
+
break
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
at
|
|
108
|
+
end
|
|
109
|
+
|
|
85
110
|
# given a range, select all tags in that range the selected tags are
|
|
86
111
|
# shifted such that the start index of the new tag (0) aligns with the
|
|
87
112
|
# given start index in the range
|
|
@@ -127,6 +152,10 @@ module Contrast
|
|
|
127
152
|
# collection. If the given range touches an existing tag,
|
|
128
153
|
# we'll combine the two, adjusting the existing one and
|
|
129
154
|
# dropping this new one.
|
|
155
|
+
#
|
|
156
|
+
# @param label [String] the name of the tag
|
|
157
|
+
# @param range [Contrast::Agent::Assess::AdjustedSpan] the span that
|
|
158
|
+
# the tag covers
|
|
130
159
|
def add_tag label, range
|
|
131
160
|
length = range.stop - range.start
|
|
132
161
|
tag = Contrast::Agent::Assess::Tag.new(length, range.start)
|
|
@@ -20,46 +20,50 @@ module Contrast
|
|
|
20
20
|
|
|
21
21
|
CS__CSRF_LOG_MSG = 'applying CSRF assess rule'
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def self.csrf_tagger patcher, preshift, _ret, _block
|
|
29
|
-
return unless rule&.enabled?
|
|
23
|
+
class << self
|
|
24
|
+
def csrf_tagger patcher, preshift, _ret, _block
|
|
25
|
+
return unless rule&.enabled?
|
|
30
26
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
idx = patcher.sources[0].to_i
|
|
28
|
+
args = preshift.args
|
|
29
|
+
return unless args&.length.to_i > idx
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
sql = args[idx]
|
|
32
|
+
return unless sql
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
with_contrast_scope do
|
|
35
|
+
logger.debug(nil, CS__CSRF_LOG_MSG)
|
|
36
|
+
rule.record_db_state_change(
|
|
37
|
+
Contrast::Agent::REQUEST_TRACKER.current,
|
|
38
|
+
sql)
|
|
39
|
+
end
|
|
43
40
|
end
|
|
44
|
-
end
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
def apply_csrf_rule sql
|
|
43
|
+
context = Contrast::Agent::REQUEST_TRACKER.current
|
|
48
44
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
return unless context&.app_loaded?
|
|
46
|
+
return unless ASSESS.enabled?
|
|
47
|
+
return unless sql
|
|
48
|
+
|
|
49
|
+
rule = Contrast::Agent::FeatureState.instance.assess_rule(
|
|
50
|
+
Contrast::Agent::Assess::Rule::Csrf::NAME)
|
|
51
|
+
return unless rule&.enabled?
|
|
52
|
+
|
|
53
|
+
with_contrast_scope do
|
|
54
|
+
logger.debug(CS__CSRF_LOG_MSG)
|
|
55
|
+
rule.record_db_state_change(context, sql)
|
|
56
|
+
end
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
logger.warn(e, 'Error running CSRF assess rule')
|
|
59
|
+
end
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
Contrast::Agent::Assess::Rule::Csrf::NAME)
|
|
55
|
-
return unless rule&.enabled?
|
|
61
|
+
private
|
|
56
62
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
def rule
|
|
64
|
+
@_rule ||= Contrast::Agent::FeatureState.instance.assess_rule(
|
|
65
|
+
Contrast::Agent::Assess::Rule::Csrf::NAME)
|
|
60
66
|
end
|
|
61
|
-
rescue StandardError => e
|
|
62
|
-
logger.warn(e, 'Error running CSRF assess rule')
|
|
63
67
|
end
|
|
64
68
|
end
|
|
65
69
|
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
|
2
|
-
# frozen_string_literal:
|
|
2
|
+
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
cs__scoped_require 'ripper'
|
|
5
|
-
cs__scoped_require 'contrast/
|
|
5
|
+
cs__scoped_require 'contrast/extensions/ruby_core/module'
|
|
6
6
|
cs__scoped_require 'contrast/components/interface'
|
|
7
7
|
|
|
8
8
|
# This method is left purposefully at the top level namespace. Moving it
|
|
@@ -17,12 +17,12 @@ cs__scoped_require 'contrast/components/interface'
|
|
|
17
17
|
def unbound_eval class_name, content
|
|
18
18
|
# Yuck, this is a top-level method that has to break encapsulation
|
|
19
19
|
# in order to access scoping!
|
|
20
|
-
Contrast::Components::Scope::COMPONENT_INTERFACE.scope_for_current_ec.enter_contrast_scope
|
|
20
|
+
Contrast::Components::Scope::COMPONENT_INTERFACE.scope_for_current_ec.enter_contrast_scope!
|
|
21
21
|
eval(content) # rubocop:disable Security/Eval
|
|
22
22
|
rescue Exception # rubocop:disable Lint/RescueException
|
|
23
23
|
Contrast::Agent::SettingsState.log_error("Unable to perform unbound eval of new content for #{ class_name }.")
|
|
24
24
|
ensure
|
|
25
|
-
Contrast::Components::Scope::COMPONENT_INTERFACE.scope_for_current_ec.exit_contrast_scope
|
|
25
|
+
Contrast::Components::Scope::COMPONENT_INTERFACE.scope_for_current_ec.exit_contrast_scope!
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
module Contrast
|
|
@@ -32,10 +32,13 @@ module Contrast
|
|
|
32
32
|
# @deprecated Changes to this class are discouraged as this approach is
|
|
33
33
|
# being phased out with support for those language versions.
|
|
34
34
|
class ClassReopener
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
include Contrast::Components::Interface
|
|
36
|
+
access_component :logging
|
|
37
|
+
|
|
38
|
+
END_NEW_LINE = "end\n"
|
|
39
|
+
PROTECTED_WITH_NEW_LINE = "protected\n"
|
|
40
|
+
PRIVATE_WITH_NEW_LINE = "private\n"
|
|
41
|
+
CLASS_SELF_LINE = "class << self\n"
|
|
39
42
|
|
|
40
43
|
attr_reader :public_singleton_methods, :class_module_path, :class_name, :files, :is_class, :name_space,
|
|
41
44
|
:public_instance_methods, :protected_instance_methods, :private_instance_methods, :locations
|
|
@@ -55,25 +58,9 @@ module Contrast
|
|
|
55
58
|
gather_modules
|
|
56
59
|
end
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
segments = class_module_path.split(Contrast::Utils::ObjectShare::DOUBLE_COLON)
|
|
62
|
-
@class_name = segments.last
|
|
63
|
-
current = nil
|
|
64
|
-
segments[0..-2].each do |chunk|
|
|
65
|
-
defined = current ? current.cs__const_defined?(chunk) : Module.cs__const_defined?(chunk)
|
|
66
|
-
next unless defined
|
|
67
|
-
|
|
68
|
-
current = current ? current.cs__const_get(chunk) : Module.cs__const_get(chunk)
|
|
69
|
-
if current.is_a?(Class)
|
|
70
|
-
name_space << [chunk, Class]
|
|
71
|
-
elsif current.is_a?(Module)
|
|
72
|
-
name_space << [chunk, Module]
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
61
|
+
# Indicates if any methods were rewritten for this class.
|
|
62
|
+
#
|
|
63
|
+
# @return [Boolean]
|
|
77
64
|
def staged_changes?
|
|
78
65
|
public_singleton_methods.any? ||
|
|
79
66
|
public_instance_methods.any? ||
|
|
@@ -81,18 +68,34 @@ module Contrast
|
|
|
81
68
|
private_instance_methods.any?
|
|
82
69
|
end
|
|
83
70
|
|
|
71
|
+
# Indicates if this class was written from the given location.
|
|
72
|
+
#
|
|
73
|
+
# @param source_location [Array<String, Integer>] the result of a
|
|
74
|
+
# Method#source_location call
|
|
75
|
+
# @return [Boolean]
|
|
84
76
|
def written_from_location? source_location
|
|
85
77
|
return false unless source_location
|
|
86
78
|
|
|
87
79
|
file = source_location[0]
|
|
88
80
|
location = source_location[1]
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
locations[file].include?(location)
|
|
82
|
+
end
|
|
91
83
|
|
|
92
|
-
|
|
93
|
-
|
|
84
|
+
# Marks that this class was written from the given location.
|
|
85
|
+
#
|
|
86
|
+
# @param source_location [Array<String, Integer>] the result of a
|
|
87
|
+
# Method#source_location call
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
def written_from_location! source_location
|
|
90
|
+
return false unless source_location
|
|
91
|
+
|
|
92
|
+
file = source_location[0]
|
|
93
|
+
location = source_location[1]
|
|
94
|
+
locations[file] << location
|
|
94
95
|
end
|
|
95
96
|
|
|
97
|
+
# Evaluate the patches that have been staged for this class, replacing
|
|
98
|
+
# the method definitions with those our rewrite.
|
|
96
99
|
def commit_patches
|
|
97
100
|
return unless staged_changes?
|
|
98
101
|
|
|
@@ -101,46 +104,86 @@ module Contrast
|
|
|
101
104
|
unbound_eval(class_name, content) if !!valid && !class_name.empty?
|
|
102
105
|
end
|
|
103
106
|
|
|
107
|
+
# Find the sourcecode of the method at the given location and return it
|
|
108
|
+
# if it is complete and compilable.
|
|
109
|
+
#
|
|
110
|
+
# @param location [Array<String, Integer>] the result of a
|
|
111
|
+
# Method#source_location call
|
|
112
|
+
# @param method_name [Symbol] the name of the method defined at the given
|
|
113
|
+
# location
|
|
114
|
+
# @return [String, nil] the code defining the method or nil if no valid
|
|
115
|
+
# code could be found.
|
|
104
116
|
def source_code location, method_name
|
|
105
117
|
file_name = location[0]
|
|
106
118
|
line_number = location[1]
|
|
107
119
|
return unless file_name && line_number
|
|
120
|
+
return unless File.exist?(file_name) && File.readable?(file_name)
|
|
121
|
+
return if File.empty?(file_name)
|
|
108
122
|
|
|
109
|
-
unless files.key?(file_name)
|
|
110
|
-
return unless File.exist?(file_name)
|
|
111
|
-
|
|
112
|
-
files[file_name] = File.readlines(file_name)
|
|
113
|
-
end
|
|
114
|
-
|
|
123
|
+
files[file_name] = File.readlines(file_name) unless files.key?(file_name)
|
|
115
124
|
lines = files[file_name]
|
|
116
|
-
|
|
117
|
-
old_verbose = $VERBOSE
|
|
118
|
-
$VERBOSE = nil
|
|
119
|
-
code = ''
|
|
120
|
-
complete = false
|
|
125
|
+
code = +''
|
|
121
126
|
# location#line_number is 1 based, arrays are 0 based
|
|
122
127
|
line_number -= 1
|
|
123
128
|
lines[line_number..-1].each do |line|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
code.gsub(/\#\{.*?\}/, 'temp')
|
|
132
|
-
end
|
|
129
|
+
code << line
|
|
130
|
+
next unless compiles?(code)
|
|
131
|
+
break if complete?(code)
|
|
132
|
+
end
|
|
133
|
+
unless complete?(code) && compiles?(code)
|
|
134
|
+
logger.warn("Failed to capture #{ method_name } in #{ file_name } at #{ line_number } for rewriting.")
|
|
135
|
+
return
|
|
133
136
|
end
|
|
134
|
-
$VERBOSE = old_verbose
|
|
135
|
-
raise SyntaxError, "Failure: method #{ method_name } in #{ file_name } at #{ line_number }" unless complete
|
|
136
|
-
|
|
137
137
|
code
|
|
138
138
|
end
|
|
139
139
|
|
|
140
140
|
private
|
|
141
141
|
|
|
142
|
+
def gather_modules
|
|
143
|
+
return if class_module_path.nil?
|
|
144
|
+
|
|
145
|
+
segments = class_module_path.split(Contrast::Utils::ObjectShare::DOUBLE_COLON)
|
|
146
|
+
@class_name = segments.last
|
|
147
|
+
current = nil
|
|
148
|
+
segments[0..-2].each do |chunk|
|
|
149
|
+
defined = current ? current.cs__const_defined?(chunk) : Module.cs__const_defined?(chunk)
|
|
150
|
+
next unless defined
|
|
151
|
+
|
|
152
|
+
current = current ? current.cs__const_get(chunk) : Module.cs__const_get(chunk)
|
|
153
|
+
if current.is_a?(Class)
|
|
154
|
+
name_space << [chunk, Class]
|
|
155
|
+
elsif current.is_a?(Module)
|
|
156
|
+
name_space << [chunk, Module]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# code which ends with a , or \ is incomplete.
|
|
162
|
+
#
|
|
163
|
+
# @param code [String] the text to determine if complete
|
|
164
|
+
# @return [Boolean]
|
|
165
|
+
def complete? code
|
|
166
|
+
code !~ /[,\\]\s*\z/
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# code which does not resolve to RubyVM::InstructionSequence does not
|
|
170
|
+
# compile
|
|
171
|
+
#
|
|
172
|
+
# @param code [String] the text to determine if compiles
|
|
173
|
+
# @return [Boolean]
|
|
174
|
+
def compiles? code
|
|
175
|
+
old_verbose = $VERBOSE
|
|
176
|
+
$VERBOSE = nil
|
|
177
|
+
RubyVM::InstructionSequence.compile(code)
|
|
178
|
+
true
|
|
179
|
+
rescue SyntaxError => _e
|
|
180
|
+
false
|
|
181
|
+
ensure
|
|
182
|
+
$VERBOSE = old_verbose
|
|
183
|
+
end
|
|
184
|
+
|
|
142
185
|
def build_content
|
|
143
|
-
content = ''
|
|
186
|
+
content = +''
|
|
144
187
|
name_space.each do |arr|
|
|
145
188
|
name = arr[0]
|
|
146
189
|
type = arr[1]
|