contrast-agent 4.2.0 → 4.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +1 -0
- data/ext/cs__assess_marshal_module/cs__assess_marshal_module.c +22 -10
- data/ext/cs__assess_marshal_module/cs__assess_marshal_module.h +4 -3
- data/lib/contrast/agent.rb +5 -1
- data/lib/contrast/agent/assess.rb +0 -9
- data/lib/contrast/agent/assess/contrast_event.rb +49 -132
- data/lib/contrast/agent/assess/contrast_object.rb +54 -0
- data/lib/contrast/agent/assess/events/source_event.rb +4 -9
- data/lib/contrast/agent/assess/finalizers/hash.rb +7 -0
- data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +17 -3
- data/lib/contrast/agent/assess/policy/patcher.rb +4 -3
- data/lib/contrast/agent/assess/policy/policy_node.rb +31 -59
- data/lib/contrast/agent/assess/policy/preshift.rb +3 -3
- data/lib/contrast/agent/assess/policy/propagation_method.rb +41 -32
- data/lib/contrast/agent/assess/policy/propagation_node.rb +12 -24
- data/lib/contrast/agent/assess/policy/propagator/append.rb +29 -15
- data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -2
- data/lib/contrast/agent/assess/policy/propagator/custom.rb +1 -1
- data/lib/contrast/agent/assess/policy/propagator/database_write.rb +21 -18
- data/lib/contrast/agent/assess/policy/propagator/insert.rb +1 -2
- data/lib/contrast/agent/assess/policy/propagator/keep.rb +1 -2
- data/lib/contrast/agent/assess/policy/propagator/match_data.rb +3 -2
- data/lib/contrast/agent/assess/policy/propagator/next.rb +1 -2
- data/lib/contrast/agent/assess/policy/propagator/prepend.rb +1 -2
- data/lib/contrast/agent/assess/policy/propagator/remove.rb +2 -4
- data/lib/contrast/agent/assess/policy/propagator/replace.rb +1 -2
- data/lib/contrast/agent/assess/policy/propagator/reverse.rb +1 -2
- data/lib/contrast/agent/assess/policy/propagator/select.rb +3 -4
- data/lib/contrast/agent/assess/policy/propagator/splat.rb +25 -17
- data/lib/contrast/agent/assess/policy/propagator/split.rb +83 -120
- data/lib/contrast/agent/assess/policy/propagator/substitution.rb +41 -25
- data/lib/contrast/agent/assess/policy/propagator/trim.rb +3 -7
- data/lib/contrast/agent/assess/policy/source_method.rb +2 -14
- data/lib/contrast/agent/assess/policy/trigger/reflected_xss.rb +5 -8
- data/lib/contrast/agent/assess/policy/trigger/xpath.rb +1 -1
- data/lib/contrast/agent/assess/policy/trigger_method.rb +13 -8
- data/lib/contrast/agent/assess/policy/trigger_node.rb +28 -7
- data/lib/contrast/agent/assess/policy/trigger_validation/redos_validator.rb +59 -0
- data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +2 -3
- data/lib/contrast/agent/assess/policy/trigger_validation/trigger_validation.rb +6 -4
- data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +2 -4
- data/lib/contrast/agent/assess/properties.rb +0 -2
- data/lib/contrast/agent/assess/property/tagged.rb +56 -32
- data/lib/contrast/agent/assess/tracker.rb +16 -18
- data/lib/contrast/agent/deadzone/policy/deadzone_node.rb +7 -0
- data/lib/contrast/agent/middleware.rb +134 -55
- data/lib/contrast/agent/patching/policy/after_load_patcher.rb +4 -0
- data/lib/contrast/agent/patching/policy/method_policy.rb +1 -1
- data/lib/contrast/agent/patching/policy/patch.rb +4 -4
- data/lib/contrast/agent/patching/policy/patch_status.rb +1 -1
- data/lib/contrast/agent/patching/policy/patcher.rb +51 -44
- data/lib/contrast/agent/patching/policy/trigger_node.rb +5 -2
- data/lib/contrast/agent/protect/policy/applies_deserialization_rule.rb +47 -1
- data/lib/contrast/agent/protect/policy/rule_applicator.rb +53 -0
- data/lib/contrast/agent/protect/rule/base.rb +63 -14
- data/lib/contrast/agent/protect/rule/cmd_injection.rb +3 -3
- data/lib/contrast/agent/protect/rule/default_scanner.rb +1 -4
- data/lib/contrast/agent/protect/rule/deserialization.rb +4 -1
- data/lib/contrast/agent/protect/rule/no_sqli.rb +3 -3
- data/lib/contrast/agent/protect/rule/sqli.rb +20 -14
- data/lib/contrast/agent/protect/rule/xxe.rb +32 -11
- data/lib/contrast/agent/protect/rule/xxe/entity_wrapper.rb +10 -6
- data/lib/contrast/agent/reaction_processor.rb +1 -1
- data/lib/contrast/agent/request_context.rb +12 -0
- data/lib/contrast/agent/response.rb +5 -5
- data/lib/contrast/agent/rewriter.rb +3 -3
- data/lib/contrast/agent/scope.rb +33 -13
- data/lib/contrast/agent/static_analysis.rb +13 -7
- data/lib/contrast/agent/thread.rb +1 -1
- data/lib/contrast/agent/thread_watcher.rb +20 -5
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/api/communication/messaging_queue.rb +18 -21
- data/lib/contrast/api/communication/response_processor.rb +8 -1
- data/lib/contrast/api/communication/socket_client.rb +22 -14
- data/lib/contrast/api/decorators.rb +2 -0
- data/lib/contrast/api/decorators/agent_startup.rb +58 -0
- data/lib/contrast/api/decorators/application_startup.rb +51 -0
- data/lib/contrast/api/decorators/library.rb +1 -0
- data/lib/contrast/api/decorators/library_usage_update.rb +1 -0
- data/lib/contrast/api/decorators/route_coverage.rb +15 -5
- data/lib/contrast/api/decorators/trace_event.rb +58 -42
- data/lib/contrast/api/decorators/trace_event_object.rb +11 -3
- data/lib/contrast/api/decorators/trace_event_signature.rb +27 -5
- data/lib/contrast/api/decorators/user_input.rb +2 -1
- data/lib/contrast/common_agent_configuration.rb +1 -1
- data/lib/contrast/components/agent.rb +2 -0
- data/lib/contrast/components/app_context.rb +4 -22
- data/lib/contrast/components/assess.rb +36 -0
- data/lib/contrast/components/interface.rb +5 -3
- data/lib/contrast/components/sampling.rb +48 -6
- data/lib/contrast/components/scope.rb +23 -0
- data/lib/contrast/components/settings.rb +8 -7
- data/lib/contrast/config/assess_configuration.rb +2 -1
- data/lib/contrast/extension/assess/array.rb +1 -2
- data/lib/contrast/extension/assess/erb.rb +1 -3
- data/lib/contrast/extension/assess/exec_trigger.rb +1 -1
- data/lib/contrast/extension/assess/fiber.rb +2 -3
- data/lib/contrast/extension/assess/hash.rb +4 -2
- data/lib/contrast/extension/assess/kernel.rb +1 -2
- data/lib/contrast/extension/assess/marshal.rb +34 -26
- data/lib/contrast/extension/assess/regexp.rb +3 -8
- data/lib/contrast/extension/assess/string.rb +1 -2
- data/lib/contrast/framework/base_support.rb +51 -53
- data/lib/contrast/framework/manager.rb +16 -14
- data/lib/contrast/framework/rack/patch/session_cookie.rb +1 -1
- data/lib/contrast/framework/rack/support.rb +2 -1
- data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +1 -1
- data/lib/contrast/framework/rails/patch/rails_application_configuration.rb +1 -1
- data/lib/contrast/framework/rails/rewrite/action_controller_railties_helper_inherited.rb +1 -1
- data/lib/contrast/framework/rails/rewrite/active_record_attribute_methods_read.rb +1 -1
- data/lib/contrast/framework/rails/rewrite/active_record_time_zone_inherited.rb +1 -1
- data/lib/contrast/framework/rails/support.rb +44 -44
- data/lib/contrast/framework/sinatra/support.rb +102 -42
- data/lib/contrast/logger/application.rb +0 -3
- data/lib/contrast/logger/log.rb +31 -15
- data/lib/contrast/utils/class_util.rb +3 -1
- data/lib/contrast/utils/duck_utils.rb +1 -1
- data/lib/contrast/utils/heap_dump_util.rb +103 -87
- data/lib/contrast/utils/invalid_configuration_util.rb +21 -12
- data/lib/contrast/utils/object_share.rb +3 -3
- data/lib/contrast/utils/preflight_util.rb +1 -1
- data/lib/contrast/utils/resource_loader.rb +1 -1
- data/lib/contrast/utils/sha256_builder.rb +2 -2
- data/lib/contrast/utils/string_utils.rb +1 -1
- data/lib/contrast/utils/tag_util.rb +9 -13
- data/resources/assess/policy.json +12 -18
- data/resources/deadzone/policy.json +150 -0
- data/resources/protect/policy.json +12 -0
- data/ruby-agent.gemspec +60 -19
- data/service_executables/VERSION +1 -1
- data/service_executables/linux/contrast-service +0 -0
- data/service_executables/mac/contrast-service +0 -0
- metadata +124 -112
- data/lib/contrast/agent/assess/rule.rb +0 -18
- data/lib/contrast/agent/assess/rule/base.rb +0 -52
- data/lib/contrast/agent/assess/rule/redos.rb +0 -67
- data/lib/contrast/framework/sinatra/patch/base.rb +0 -83
- data/lib/contrast/framework/sinatra/patch/support.rb +0 -27
- data/lib/contrast/utils/prevent_serialization.rb +0 -52
@@ -92,14 +92,11 @@ module Contrast
|
|
92
92
|
end
|
93
93
|
|
94
94
|
def string_sub parent_events, self_tracked, preshift, ret, incoming, incoming_tracked, global
|
95
|
-
properties = Contrast::Agent::Assess::Tracker.properties(ret)
|
96
|
-
return unless properties
|
95
|
+
return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))
|
97
96
|
|
98
97
|
incoming_properties = Contrast::Agent::Assess::Tracker.properties(incoming)
|
99
|
-
|
100
|
-
parent_events << parent_event if parent_event
|
98
|
+
parent_events << incoming_properties&.event if incoming_properties&.event
|
101
99
|
|
102
|
-
pattern = preshift.args[0]
|
103
100
|
source = preshift.object
|
104
101
|
|
105
102
|
# We can't efficiently find the places that things were
|
@@ -112,6 +109,34 @@ module Contrast
|
|
112
109
|
|
113
110
|
# if it's just a straight insert, that we can do
|
114
111
|
# Copy the tags from us to the return
|
112
|
+
ranges = find_string_sub_insert(properties, preshift, incoming, ret, global)
|
113
|
+
|
114
|
+
properties.delete_tags_at_ranges(ranges)
|
115
|
+
properties.shift_tags(ranges)
|
116
|
+
return unless incoming_tracked
|
117
|
+
return unless incoming_properties
|
118
|
+
|
119
|
+
tags = incoming_properties.tag_keys
|
120
|
+
ranges.each do |range|
|
121
|
+
tags.each do |tag|
|
122
|
+
properties.add_tag(tag, range)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Find the points at which the new String was placed into the original
|
128
|
+
#
|
129
|
+
# @param properties [Contrast::Agent::Assess::Properties] the Properties of the ret
|
130
|
+
# @param preshift [Contrast::Agent::Assess::PreShift] the capture of the state of the code just prior to
|
131
|
+
# the invocation of the patched method
|
132
|
+
# @param incoming [String] the new String going into the substitution
|
133
|
+
# @param ret [String] the result of the substitution
|
134
|
+
# @param global [Boolean] if this was a global or single substitution
|
135
|
+
# @return [Array<Range>] the Ranges where substitution occurred
|
136
|
+
def find_string_sub_insert properties, preshift, incoming, ret, global
|
137
|
+
pattern = preshift.args[0]
|
138
|
+
source = preshift.object
|
139
|
+
|
115
140
|
properties.copy_from(source, ret)
|
116
141
|
# Figure out where inserts occurred
|
117
142
|
last_idx = 0
|
@@ -127,36 +152,27 @@ module Contrast
|
|
127
152
|
ranges << (start_index...end_index)
|
128
153
|
break unless global
|
129
154
|
end
|
130
|
-
|
131
|
-
properties.shift_tags(ranges)
|
132
|
-
return unless incoming_tracked
|
133
|
-
return unless incoming_properties
|
134
|
-
|
135
|
-
tags = incoming_properties.tag_keys
|
136
|
-
ranges.each do |range|
|
137
|
-
tags.each do |tag|
|
138
|
-
properties.add_tag(tag, range)
|
139
|
-
end
|
140
|
-
end
|
155
|
+
ranges
|
141
156
|
end
|
142
157
|
|
143
158
|
def block_sub self_tracked, source, ret
|
144
|
-
|
145
|
-
|
159
|
+
return unless self_tracked
|
160
|
+
|
161
|
+
properties = Contrast::Agent::Assess::Tracker.properties!(ret)
|
162
|
+
properties&.splat_from(source, ret)
|
146
163
|
end
|
147
164
|
|
148
165
|
def hash_sub self_tracked, source, ret
|
149
|
-
|
150
|
-
|
166
|
+
return unless self_tracked
|
167
|
+
|
168
|
+
properties = Contrast::Agent::Assess::Tracker.properties!(ret)
|
169
|
+
properties&.splat_from(source, ret)
|
151
170
|
end
|
152
171
|
|
153
172
|
def pattern_gsub parent_events, preshift, ret
|
154
|
-
properties = Contrast::Agent::Assess::Tracker.properties(ret)
|
155
|
-
return unless properties
|
156
|
-
|
157
173
|
source = preshift.object
|
158
|
-
source_properties = Contrast::Agent::Assess::Tracker.properties(source)
|
159
|
-
return unless
|
174
|
+
return unless (source_properties = Contrast::Agent::Assess::Tracker.properties(source))
|
175
|
+
return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))
|
160
176
|
|
161
177
|
source_properties.tag_keys.each do |key|
|
162
178
|
properties.add_tag(key, 0...1)
|
@@ -11,13 +11,11 @@ module Contrast
|
|
11
11
|
# Disclaimer: there may be a better way, but we're
|
12
12
|
# in a 'get it work' state. hopefully, we'll be in
|
13
13
|
# a 'get it right' state soon.
|
14
|
-
|
14
|
+
module Trim
|
15
15
|
class << self
|
16
16
|
def tr_tagger patcher, preshift, ret, _block
|
17
17
|
return ret unless ret && !ret.empty?
|
18
|
-
|
19
|
-
properties = Contrast::Agent::Assess::Tracker.properties(ret)
|
20
|
-
return unless properties
|
18
|
+
return ret unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))
|
21
19
|
|
22
20
|
source = preshift.object
|
23
21
|
args = preshift.args
|
@@ -59,9 +57,7 @@ module Contrast
|
|
59
57
|
|
60
58
|
def tr_s_tagger patcher, preshift, ret, _block
|
61
59
|
return unless ret && !ret.empty?
|
62
|
-
|
63
|
-
properties = Contrast::Agent::Assess::Tracker.properties(ret)
|
64
|
-
return unless properties
|
60
|
+
return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))
|
65
61
|
|
66
62
|
source = preshift.object
|
67
63
|
args = preshift.args
|
@@ -178,8 +178,8 @@ module Contrast
|
|
178
178
|
# don't apply second source -- probably needs tuning later if we
|
179
179
|
# use more than 'UNTRUSTED' in our sources
|
180
180
|
return if Contrast::Agent::Assess::Tracker.tracked?(target)
|
181
|
+
return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
|
181
182
|
|
182
|
-
properties = Contrast::Agent::Assess::Tracker.properties(target)
|
183
183
|
# otherwise for each tag this source_node applies, create a tag range
|
184
184
|
# on the target object
|
185
185
|
# I realize this looping is counter-intuitive from the above
|
@@ -243,19 +243,7 @@ module Contrast
|
|
243
243
|
when Contrast::Utils::ObjectShare::OBJECT_KEY
|
244
244
|
object
|
245
245
|
else
|
246
|
-
|
247
|
-
args[source_target]
|
248
|
-
# If this isn't an index param, it's a named one. R.I.P.
|
249
|
-
else
|
250
|
-
arg = nil
|
251
|
-
args.each do |search|
|
252
|
-
next unless search.is_a?(Hash)
|
253
|
-
|
254
|
-
arg = search[source_target]
|
255
|
-
break if arg
|
256
|
-
end
|
257
|
-
arg
|
258
|
-
end
|
246
|
+
args[source_target]
|
259
247
|
end
|
260
248
|
end
|
261
249
|
|
@@ -25,14 +25,13 @@ module Contrast
|
|
25
25
|
TEMPLATE_PROPAGATION_NODE = Contrast::Agent::Assess::Policy::PropagationNode.new(NODE_HASH)
|
26
26
|
|
27
27
|
def xss_tilt_trigger context, trigger_node, _source, object, ret, *args
|
28
|
-
properties = Contrast::Agent::Assess::Tracker.properties(ret)
|
29
|
-
return unless properties
|
28
|
+
return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))
|
30
29
|
|
31
30
|
scope = args[0]
|
32
31
|
erb_template_prerender = object.instance_variable_get(:@data)
|
33
32
|
interpolated_inputs = []
|
34
|
-
handle_binding_variables(scope, erb_template_prerender, ret, interpolated_inputs)
|
35
|
-
handle_local_variables(args, erb_template_prerender, ret, interpolated_inputs)
|
33
|
+
handle_binding_variables(scope, erb_template_prerender, ret, properties, interpolated_inputs)
|
34
|
+
handle_local_variables(args, erb_template_prerender, ret, properties, interpolated_inputs)
|
36
35
|
properties.build_event(TEMPLATE_PROPAGATION_NODE, ret, erb_template_prerender, ret, interpolated_inputs)
|
37
36
|
unless interpolated_inputs.empty?
|
38
37
|
current_event = properties.event
|
@@ -53,8 +52,7 @@ module Contrast
|
|
53
52
|
|
54
53
|
private
|
55
54
|
|
56
|
-
def handle_binding_variables scope, erb_template_prerender, ret, interpolated_inputs
|
57
|
-
properties = Contrast::Agent::Assess::Tracker.properties(ret)
|
55
|
+
def handle_binding_variables scope, erb_template_prerender, ret, properties, interpolated_inputs
|
58
56
|
binding_variables = scope.instance_variables
|
59
57
|
|
60
58
|
binding_variables.each do |bound_variable_sym|
|
@@ -71,8 +69,7 @@ module Contrast
|
|
71
69
|
end
|
72
70
|
end
|
73
71
|
|
74
|
-
def handle_local_variables args, erb_template_prerender, ret, interpolated_inputs
|
75
|
-
properties = Contrast::Agent::Assess::Tracker.properties(ret)
|
72
|
+
def handle_local_variables args, erb_template_prerender, ret, properties, interpolated_inputs
|
76
73
|
locals = args[1]
|
77
74
|
locals.each do |local_name, local_value|
|
78
75
|
next unless Contrast::Agent::Assess::Tracker.tracked?(local_value)
|
@@ -101,18 +101,12 @@ module Contrast
|
|
101
101
|
# conditions were not met
|
102
102
|
def build_finding context, trigger_node, source, object, ret, *args
|
103
103
|
return unless Contrast::Agent::Assess::Policy::TriggerValidation.valid?(trigger_node, object, ret, args)
|
104
|
-
|
105
|
-
request = context.request
|
106
|
-
env = request.env
|
107
|
-
return if defined?(ActionController::Live) &&
|
108
|
-
env &&
|
109
|
-
env['action_controller.instance'].cs__class.included_modules.include?(ActionController::Live)
|
104
|
+
return unless reportable?(context)
|
110
105
|
|
111
106
|
finding = Contrast::Api::Dtm::Finding.new
|
112
107
|
finding.rule_id = Contrast::Utils::StringUtils.protobuf_safe_string(trigger_node.rule_id)
|
113
108
|
build_from_source(finding, source)
|
114
|
-
|
115
|
-
finding.events << trigger_event
|
109
|
+
finding.events << Contrast::Agent::Assess::Events::EventFactory.build(trigger_node, source, object, ret, args).to_dtm_event
|
116
110
|
build_hash(finding, source)
|
117
111
|
finding.routes << context.route if context.route
|
118
112
|
finding.version = determine_compliance_version(finding)
|
@@ -127,6 +121,17 @@ module Contrast
|
|
127
121
|
|
128
122
|
private
|
129
123
|
|
124
|
+
# A request is reportable if it is not from ActionController::Live
|
125
|
+
#
|
126
|
+
# @param context [Contrast::Agent::RequestContext] the current request context
|
127
|
+
# @return [Boolean]
|
128
|
+
def reportable? context
|
129
|
+
env = context.request.env
|
130
|
+
!(defined?(ActionController::Live) &&
|
131
|
+
env &&
|
132
|
+
env['action_controller.instance'].cs__class.included_modules.include?(ActionController::Live))
|
133
|
+
end
|
134
|
+
|
130
135
|
# This is our method that actually checks the taint on the object
|
131
136
|
# our trigger_node targets.
|
132
137
|
#
|
@@ -185,11 +185,7 @@ module Contrast
|
|
185
185
|
# @return [Array<Contrast::Agent::Assess::Tag>] the ranges satisfied
|
186
186
|
# by the given conditions
|
187
187
|
def ranges_with_all_tags length, properties, required_tags
|
188
|
-
|
189
|
-
# all the required tags, we can just return here.
|
190
|
-
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless properties.tracked?
|
191
|
-
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless required_tags&.any?
|
192
|
-
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless required_tags.all? { |tag| properties.tag_keys.include?(tag) }
|
188
|
+
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless matches_tags?(properties, required_tags)
|
193
189
|
|
194
190
|
ranges = []
|
195
191
|
chunking = false
|
@@ -229,8 +225,7 @@ module Contrast
|
|
229
225
|
# by the given conditions
|
230
226
|
def ranges_with_any_tag properties, tags
|
231
227
|
# if there aren't any all_tags or tags, break early
|
232
|
-
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless properties
|
233
|
-
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tags&.any?
|
228
|
+
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless search_tags?(properties, tags)
|
234
229
|
|
235
230
|
ranges = []
|
236
231
|
tags.each do |desired|
|
@@ -243,6 +238,32 @@ module Contrast
|
|
243
238
|
end
|
244
239
|
ranges
|
245
240
|
end
|
241
|
+
|
242
|
+
# We should only try to match tags on properties if those properties have any tags (are tracked) and there
|
243
|
+
# are tags to try and match on. Some rules, like regexp rules, have no tags. Some rules, like trigger, have
|
244
|
+
# no properties.
|
245
|
+
#
|
246
|
+
# @param properties [Contrast::Agent::Assess::Properties] the properties to check for the tags
|
247
|
+
# @param tags [Set<String>] the list of tags on which to match
|
248
|
+
# @return [Boolean] if the given properties has instances of every tag in tags
|
249
|
+
def search_tags? properties, tags
|
250
|
+
return false unless properties.tracked?
|
251
|
+
return false unless tags&.any?
|
252
|
+
|
253
|
+
true
|
254
|
+
end
|
255
|
+
|
256
|
+
# Determine if the given properties have instances of all the given tags or not.
|
257
|
+
#
|
258
|
+
# @param properties [Contrast::Agent::Assess::Properties] the properties to check for the tags
|
259
|
+
# @param tags [Set<String>] the list of tags on which to match
|
260
|
+
# @return [Boolean] if the given properties has instances of every tag in tags
|
261
|
+
def matches_tags? properties, tags
|
262
|
+
return false unless search_tags?(properties, tags)
|
263
|
+
return false unless tags.all? { |tag| properties.tag_keys.include?(tag) }
|
264
|
+
|
265
|
+
true
|
266
|
+
end
|
246
267
|
end
|
247
268
|
end
|
248
269
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Contrast
|
5
|
+
module Agent
|
6
|
+
module Assess
|
7
|
+
module Policy
|
8
|
+
module TriggerValidation
|
9
|
+
# Validator used to assert a REDOS finding is actually vulnerable
|
10
|
+
# before serializing that finding as a DTM to report to the service.
|
11
|
+
module REDOSValidator
|
12
|
+
RULE_NAME = 'redos'
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def valid? _patcher, object, _ret, args
|
16
|
+
# Can arrive here from either:
|
17
|
+
# regexp =~ string
|
18
|
+
# string =~ regexp
|
19
|
+
# regexp.match string
|
20
|
+
#
|
21
|
+
# Thus object/args[0] can be string/regexp or regexp/string.
|
22
|
+
regexp = object.is_a?(Regexp) ? object : args[0]
|
23
|
+
|
24
|
+
# regexp must be exploitable.
|
25
|
+
return false unless regexp_vulnerable?(regexp)
|
26
|
+
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
VULNERABLE_PATTERN = /[\[(].*?[\[(].*?[\])][*+?].*?[\])][*+?]/.cs__freeze
|
33
|
+
|
34
|
+
# Does the regexp
|
35
|
+
# https://bitbucket.org/contrastsecurity/assess-specifications/src/master/rules/dataflow/redos.md
|
36
|
+
def regexp_vulnerable? regexp
|
37
|
+
# A pattern is considered vulnerable if it has 2 or more levels of nested multi-matching.
|
38
|
+
# A level is defined as any set of opening and closing control characters immediately followed by a multi match control character.
|
39
|
+
# A control character is defined as one of the OPENING_CHARS, CLOSING_CHARS,
|
40
|
+
# or MULTI_MATCH_CHARS that is not immediately preceded by an escaping \ character.
|
41
|
+
# OPENING_CHARS are ( and [ CLOSING_CHARS are ) and ] MULTI_MATCH_CHARS are +, *, and ?
|
42
|
+
|
43
|
+
# Nota bene about Regexp#to_s: it doesn't necessarily give you the original Regexp back
|
44
|
+
# (in the sense of `my_str == Regexp.new(my_str).to_s`), it gives you a Regexp that
|
45
|
+
# will have the same functional characteristics as the original.
|
46
|
+
# Regexp#inspect gives you a "more nicely formatted" version than #to_s.
|
47
|
+
# Regexp#source will give you the original source.
|
48
|
+
|
49
|
+
# Use #match? because it doesn't fill out global variables
|
50
|
+
# in the way match or =~ do.
|
51
|
+
VULNERABLE_PATTERN.match? regexp.source
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -9,7 +9,7 @@ module Contrast
|
|
9
9
|
# Validator used to assert a SSRF finding is actually vulnerable
|
10
10
|
# before serializing that finding as a DTM to report to the service.
|
11
11
|
module SSRFValidator
|
12
|
-
|
12
|
+
RULE_NAME = 'ssrf'
|
13
13
|
URL_PATTERN =
|
14
14
|
%r{(?<protocol>http|https|ftp|sftp|telnet|gopher|rtsp|rtsps|ssh|svn)://(?<host>[^/?]+)(?<path>/?[^?]*)(?<query_string>\?.*)?}i.cs__freeze
|
15
15
|
# The Net::HTTP class validates host format on instantiation. Since
|
@@ -23,7 +23,6 @@ module Contrast
|
|
23
23
|
# querystring
|
24
24
|
# https://bitbucket.org/contrastsecurity/assess-specifications/src/master/rules/dataflow/server_side_request_forgery.md
|
25
25
|
def self.valid? patcher, _object, _ret, args
|
26
|
-
return true unless SSRF_RULE == patcher&.rule_id
|
27
26
|
return true if patcher.id.to_s.start_with?(PATH_ONLY_PATCH_MARKER)
|
28
27
|
|
29
28
|
url = args[0].to_s
|
@@ -39,7 +38,7 @@ module Contrast
|
|
39
38
|
finish ||= url.length
|
40
39
|
|
41
40
|
properties = Contrast::Agent::Assess::Tracker.properties(args[0])
|
42
|
-
properties
|
41
|
+
properties&.any_tags_between?(start, finish)
|
43
42
|
end
|
44
43
|
end
|
45
44
|
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
|
4
4
|
require 'contrast/agent/assess/policy/trigger_validation/ssrf_validator'
|
5
5
|
require 'contrast/agent/assess/policy/trigger_validation/xss_validator'
|
6
|
+
require 'contrast/agent/assess/policy/trigger_validation/redos_validator'
|
6
7
|
|
7
8
|
module Contrast
|
8
9
|
module Agent
|
@@ -15,7 +16,8 @@ module Contrast
|
|
15
16
|
module TriggerValidation
|
16
17
|
VALIDATORS = [
|
17
18
|
Contrast::Agent::Assess::Policy::TriggerValidation::SSRFValidator,
|
18
|
-
Contrast::Agent::Assess::Policy::TriggerValidation::XSSValidator
|
19
|
+
Contrast::Agent::Assess::Policy::TriggerValidation::XSSValidator,
|
20
|
+
Contrast::Agent::Assess::Policy::TriggerValidation::REDOSValidator
|
19
21
|
].cs__freeze
|
20
22
|
|
21
23
|
# Determines if the conditions in which this trigger was called are
|
@@ -32,9 +34,9 @@ module Contrast
|
|
32
34
|
# @return [Boolean] if the conditions are valid for the generation of
|
33
35
|
# a Contrast::Api::Dtm::Finding
|
34
36
|
def self.valid? patcher, object, ret, args
|
35
|
-
VALIDATORS.
|
36
|
-
|
37
|
-
|
37
|
+
specific_validator = VALIDATORS.find { |validator| validator::RULE_NAME == patcher&.rule_id }
|
38
|
+
return specific_validator.valid?(patcher, object, ret, args) if specific_validator
|
39
|
+
|
38
40
|
true
|
39
41
|
end
|
40
42
|
end
|
@@ -10,7 +10,7 @@ module Contrast
|
|
10
10
|
# vulnerable before serializing that finding as a DTM to report to
|
11
11
|
# the service.
|
12
12
|
module XSSValidator
|
13
|
-
|
13
|
+
RULE_NAME = 'reflected-xss'
|
14
14
|
SAFE_CONTENT_TYPES = %w[
|
15
15
|
/csv
|
16
16
|
/javascript
|
@@ -23,9 +23,7 @@ module Contrast
|
|
23
23
|
# A finding is valid for XSS if the response type is not one of
|
24
24
|
# those assumed to be safe
|
25
25
|
# https://bitbucket.org/contrastsecurity/assess-specifications/src/master/rules/dataflow/reflected_xss.md
|
26
|
-
def self.valid?
|
27
|
-
return true unless XSS_RULE == patcher&.rule_id
|
28
|
-
|
26
|
+
def self.valid? _patcher, _object, _ret, _args
|
29
27
|
content_type = Contrast::Agent::REQUEST_TRACKER.current&.response&.content_type
|
30
28
|
return true unless content_type
|
31
29
|
|
@@ -6,7 +6,6 @@ require 'set'
|
|
6
6
|
require 'contrast/agent/assess/property/evented'
|
7
7
|
require 'contrast/agent/assess/property/tagged'
|
8
8
|
require 'contrast/agent/assess/property/updated'
|
9
|
-
require 'contrast/utils/prevent_serialization'
|
10
9
|
|
11
10
|
module Contrast
|
12
11
|
module Agent
|
@@ -18,7 +17,6 @@ module Contrast
|
|
18
17
|
# to properly convey the events that lead up to the state of the tracked
|
19
18
|
# user input.
|
20
19
|
class Properties
|
21
|
-
include Contrast::Utils::PreventSerialization
|
22
20
|
include Contrast::Agent::Assess::Property::Evented
|
23
21
|
include Contrast::Agent::Assess::Property::Tagged
|
24
22
|
include Contrast::Agent::Assess::Property::Updated
|