contrast-agent 4.2.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/assess/contrast_event.rb +49 -130
- data/lib/contrast/agent/assess/contrast_object.rb +51 -0
- data/lib/contrast/agent/assess/events/source_event.rb +4 -9
- 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 +13 -19
- data/lib/contrast/agent/assess/policy/propagation_node.rb +12 -24
- data/lib/contrast/agent/assess/policy/propagator/append.rb +1 -2
- 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 +1 -3
- 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 +2 -4
- data/lib/contrast/agent/assess/policy/propagator/split.rb +73 -117
- data/lib/contrast/agent/assess/policy/propagator/substitution.rb +11 -11
- 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_validation/ssrf_validator.rb +1 -1
- data/lib/contrast/agent/assess/property/tagged.rb +21 -15
- data/lib/contrast/agent/assess/rule/redos.rb +1 -1
- 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 +50 -1
- 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/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 +3 -3
- 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/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/version.rb +1 -1
- 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/trace_event.rb +19 -31
- 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/assess.rb +36 -0
- data/lib/contrast/components/interface.rb +5 -3
- data/lib/contrast/components/scope.rb +23 -0
- data/lib/contrast/components/settings.rb +3 -3
- 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 +3 -2
- 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 +2 -1
- data/lib/contrast/framework/sinatra/support.rb +3 -2
- data/lib/contrast/logger/application.rb +0 -3
- data/lib/contrast/utils/duck_utils.rb +1 -1
- data/lib/contrast/utils/heap_dump_util.rb +1 -1
- data/lib/contrast/utils/object_share.rb +3 -3
- data/lib/contrast/utils/preflight_util.rb +1 -1
- data/lib/contrast/utils/prevent_serialization.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 +9 -9
- data/resources/deadzone/policy.json +156 -0
- data/resources/protect/policy.json +12 -0
- data/ruby-agent.gemspec +9 -6
- data/service_executables/VERSION +1 -1
- data/service_executables/linux/contrast-service +0 -0
- data/service_executables/mac/contrast-service +0 -0
- metadata +68 -25
|
@@ -15,6 +15,28 @@ module Contrast
|
|
|
15
15
|
|
|
16
16
|
access_component :analysis, :logging
|
|
17
17
|
|
|
18
|
+
# Calls the actual invocation for this applicator, if required. Will
|
|
19
|
+
# attempt to transform the data as required prior to invocation and
|
|
20
|
+
# provides a common interface for those rules that have the same
|
|
21
|
+
# implementation regardless of the method patched.
|
|
22
|
+
#
|
|
23
|
+
# For those methods with different transformations depending on the
|
|
24
|
+
# method instrumented, variations of this method, including an
|
|
25
|
+
# indication of for which instrumented method they apply, will exist.
|
|
26
|
+
#
|
|
27
|
+
# @param method [Symbol] the name of the method for which this rule
|
|
28
|
+
# is invoked
|
|
29
|
+
# @param exception [Exception] any exception raised; used for rules
|
|
30
|
+
# like Padding Oracle Attack (now defunct), which determine if the
|
|
31
|
+
# number and type of exceptions are an attack
|
|
32
|
+
# @param properties [Hash] set of extra information provided by the
|
|
33
|
+
# applicator in an attempt to build a better story for the user
|
|
34
|
+
# @param object [Object] the thing on which the triggering method was
|
|
35
|
+
# invoked
|
|
36
|
+
# @param args [Array<Object>] the arguments passed to the triggering
|
|
37
|
+
# method at invocation
|
|
38
|
+
# @raise [Contrast::SecurityException] on block, will pass the
|
|
39
|
+
# exception from the rule
|
|
18
40
|
def apply_rule method, exception, properties, object, args
|
|
19
41
|
invoke(method, exception, properties, object, args)
|
|
20
42
|
rescue Contrast::SecurityException => e
|
|
@@ -25,18 +47,49 @@ module Contrast
|
|
|
25
47
|
|
|
26
48
|
protected
|
|
27
49
|
|
|
50
|
+
# Calls the actual rule for this applicator, if required. Most rules
|
|
51
|
+
# invoke this from within their apply_rule method after doing
|
|
52
|
+
# whatever transformations they need to get into this common format.
|
|
53
|
+
#
|
|
54
|
+
# @param _method [Symbol] the name of the method for which this rule
|
|
55
|
+
# is invoked
|
|
56
|
+
# @param _exception [Exception] any exception raised; used for rules
|
|
57
|
+
# like Padding Oracle Attack (now defunct), which determine if the
|
|
58
|
+
# number and type of exceptions are an attack
|
|
59
|
+
# @param _properties [Hash] set of extra information provided by the
|
|
60
|
+
# applicator in an attempt to build a better story for the user
|
|
61
|
+
# @param _object [Object] the thing on which the triggering method
|
|
62
|
+
# was invoked
|
|
63
|
+
# @param _args [Array<Object>] the arguments passed to the triggering
|
|
64
|
+
# method at invocation
|
|
65
|
+
# @raise [Contrast::SecurityException] on block, will pass the
|
|
66
|
+
# exception from the rule
|
|
28
67
|
def invoke _method, _exception, _properties, _object, _args
|
|
29
68
|
raise NoMethodError, 'This is abstract, override it.'
|
|
30
69
|
end
|
|
31
70
|
|
|
71
|
+
# The name of the rule, as expected by the Contrast Service and
|
|
72
|
+
# Contrast UI.
|
|
73
|
+
#
|
|
74
|
+
# @return [String]
|
|
32
75
|
def name
|
|
33
76
|
raise NoMethodError, 'This is abstract, override it.'
|
|
34
77
|
end
|
|
35
78
|
|
|
79
|
+
# The rule for which this applicator applies. It'll be a concrete
|
|
80
|
+
# sub-class of Contrast::Agent::Protect::Rule::Base, found based on
|
|
81
|
+
# the value of Contrast::Agent::Protect::Policy::RuleApplicator#name.
|
|
82
|
+
#
|
|
83
|
+
# @return [Contrast::Agent::Protect::Rule::Base]
|
|
36
84
|
def rule
|
|
37
85
|
PROTECT.rule name
|
|
38
86
|
end
|
|
39
87
|
|
|
88
|
+
# Should we skip analysis for this rule for this method invocation?
|
|
89
|
+
# This allows us to short circuit in those cases for which the rule
|
|
90
|
+
# will not apply.
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean]
|
|
40
93
|
def skip_analysis?
|
|
41
94
|
context = Contrast::Agent::REQUEST_TRACKER.current
|
|
42
95
|
return true unless context&.app_loaded?
|
|
@@ -10,7 +10,7 @@ module Contrast
|
|
|
10
10
|
# This is a basic rule for Protect. It's the abstract class which all other
|
|
11
11
|
# protect rules extend in order to function.
|
|
12
12
|
#
|
|
13
|
-
# @abstract Subclass and override {#prefilter}, {#infilter}, {#find_attacker}, {#postfilter}
|
|
13
|
+
# @abstract Subclass and override {#prefilter}, {#infilter}, {#find_attacker}, {#postfilter} to implement
|
|
14
14
|
class Base
|
|
15
15
|
include Contrast::Components::Interface
|
|
16
16
|
|
|
@@ -20,13 +20,19 @@ module Contrast
|
|
|
20
20
|
user_input.input_type = :UNKNOWN
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
BLOCKING_MODES = Set.new([
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
BLOCKING_MODES = Set.new([
|
|
24
|
+
Contrast::Api::Settings::ProtectionRule::Mode::BLOCK,
|
|
25
|
+
Contrast::Api::Settings::ProtectionRule::Mode::BLOCK_AT_PERIMETER
|
|
26
|
+
]).cs__freeze
|
|
27
|
+
POSTFILTER_MODES = Set.new([
|
|
28
|
+
Contrast::Api::Settings::ProtectionRule::Mode::BLOCK,
|
|
29
|
+
Contrast::Api::Settings::ProtectionRule::Mode::PERMIT,
|
|
30
|
+
Contrast::Api::Settings::ProtectionRule::Mode::MONITOR
|
|
31
|
+
]).cs__freeze
|
|
32
|
+
STACK_COLLECTION_RESULTS = Set.new([
|
|
33
|
+
Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED,
|
|
34
|
+
Contrast::Api::Dtm::AttackResult::ResponseType::MONITORED
|
|
35
|
+
]).cs__freeze
|
|
30
36
|
|
|
31
37
|
attr_reader :mode
|
|
32
38
|
|
|
@@ -116,6 +122,26 @@ module Contrast
|
|
|
116
122
|
# the current request
|
|
117
123
|
def postfilter _context; end
|
|
118
124
|
|
|
125
|
+
# A given input, candidate_string, was determined to violate a
|
|
126
|
+
# protect rule and did exploit the application, or at least made it
|
|
127
|
+
# to exploitable code in the case where we blocked the attack. As
|
|
128
|
+
# such, we need to build a result to report this violation to the
|
|
129
|
+
# Service.
|
|
130
|
+
#
|
|
131
|
+
# @param context [Contrast::Agent::RequestContext] the context of the
|
|
132
|
+
# request in which this input is evaluated.
|
|
133
|
+
# @param ia_result [Contrast::Api::Settings::InputAnalysisResult] the
|
|
134
|
+
# analysis of the input that was determined to be an attack
|
|
135
|
+
# @param result [Contrast::Api::Dtm::AttackResult, nil] previous
|
|
136
|
+
# attack result for this rule, if one exists, in the case of
|
|
137
|
+
# multiple inputs being found to violate the protection criteria
|
|
138
|
+
# @param candidate_string [String] the value of the input which may
|
|
139
|
+
# be an attack
|
|
140
|
+
# @param kwargs [Hash] key - value pairs of context individual rules
|
|
141
|
+
# need to build out details to send to the Service to tell the
|
|
142
|
+
# story of the attack
|
|
143
|
+
# @return [Contrast::Api::Dtm::AttackResult] the attack result from
|
|
144
|
+
# this input
|
|
119
145
|
def build_attack_with_match context, ia_result, result, candidate_string, **kwargs
|
|
120
146
|
result ||= build_attack_result(context)
|
|
121
147
|
update_successful_attack_response(context, ia_result, result, candidate_string)
|
|
@@ -124,6 +150,22 @@ module Contrast
|
|
|
124
150
|
result
|
|
125
151
|
end
|
|
126
152
|
|
|
153
|
+
# A given input, candidate_string, was determined to violate a
|
|
154
|
+
# protect rule but did not exploit the application. As such, we need
|
|
155
|
+
# to build a result to report this violation to the Service.
|
|
156
|
+
#
|
|
157
|
+
# @param context [Contrast::Agent::RequestContext] the context of the
|
|
158
|
+
# request in which this input is evaluated.
|
|
159
|
+
# @param ia_result [Contrast::Api::Settings::InputAnalysisResult] the
|
|
160
|
+
# analysis of the input that was determined to be an attack
|
|
161
|
+
# @param result [Contrast::Api::Dtm::AttackResult, nil] previous
|
|
162
|
+
# attack result for this rule, if one exists, in the case of
|
|
163
|
+
# multiple inputs being found to violate the protection criteria
|
|
164
|
+
# @param kwargs [Hash] key - value pairs of context individual rules
|
|
165
|
+
# need to build out details to send to the Service to tell the
|
|
166
|
+
# story of the attack
|
|
167
|
+
# @return [Contrast::Api::Dtm::AttackResult] the attack result from
|
|
168
|
+
# this input
|
|
127
169
|
def build_attack_without_match context, ia_result, result, **kwargs
|
|
128
170
|
result ||= build_attack_result(context)
|
|
129
171
|
update_perimeter_attack_response(context, ia_result, result)
|
|
@@ -132,16 +174,18 @@ module Contrast
|
|
|
132
174
|
result
|
|
133
175
|
end
|
|
134
176
|
|
|
177
|
+
# Attach the given result to the current request's context to report
|
|
178
|
+
# it to the Service
|
|
179
|
+
#
|
|
180
|
+
# @param context [Contrast::Agent::RequestContext] the context of the
|
|
181
|
+
# request in which this input is evaluated.
|
|
182
|
+
# @param result [Contrast::Api::Dtm::AttackResult]
|
|
135
183
|
def append_to_activity context, result
|
|
136
184
|
context.activity.results << result if result
|
|
137
185
|
end
|
|
138
186
|
|
|
139
187
|
protected
|
|
140
188
|
|
|
141
|
-
def build_details _input_string, _ia_result
|
|
142
|
-
raise NoMethodError, "Rule #{ name } did not implement build_details"
|
|
143
|
-
end
|
|
144
|
-
|
|
145
189
|
def mode_from_settings
|
|
146
190
|
PROTECT.rule_mode(name).tap do |mode|
|
|
147
191
|
logger.trace('Retrieving rule mode', rule: name, mode: mode)
|
|
@@ -209,6 +253,11 @@ module Contrast
|
|
|
209
253
|
result
|
|
210
254
|
end
|
|
211
255
|
|
|
256
|
+
# Set up an attack result for the current rule
|
|
257
|
+
#
|
|
258
|
+
# @param _context [Contrast::Agent::RequestContext] the context of
|
|
259
|
+
# the current request
|
|
260
|
+
# @return [Contrast::Api::Dtm::AttackResult]
|
|
212
261
|
def build_attack_result _context
|
|
213
262
|
result = Contrast::Api::Dtm::AttackResult.new
|
|
214
263
|
result.rule_id = name
|
|
@@ -226,10 +275,10 @@ module Contrast
|
|
|
226
275
|
end
|
|
227
276
|
|
|
228
277
|
def append_sample context, ia_result, result, candidate_string, **kwargs
|
|
229
|
-
return
|
|
278
|
+
return unless result
|
|
230
279
|
|
|
231
280
|
sample = build_sample(context, ia_result, candidate_string, **kwargs)
|
|
232
|
-
return
|
|
281
|
+
return unless sample
|
|
233
282
|
|
|
234
283
|
append_stack(sample, result)
|
|
235
284
|
|
|
@@ -23,10 +23,10 @@ module Contrast
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def infilter context, classname, method, command
|
|
26
|
-
return
|
|
26
|
+
return unless infilter?(context)
|
|
27
27
|
|
|
28
28
|
ia_results = gather_ia_results(context)
|
|
29
|
-
return
|
|
29
|
+
return if ia_results.empty?
|
|
30
30
|
|
|
31
31
|
if APP_CONTEXT.in_new_process?
|
|
32
32
|
logger.trace('Running cmd-injection infilter within new process - creating new context')
|
|
@@ -36,7 +36,7 @@ module Contrast
|
|
|
36
36
|
|
|
37
37
|
result = find_attacker_with_results(context, command, ia_results, **{ classname: classname, method: method })
|
|
38
38
|
result ||= report_command_execution(context, command, **{ classname: classname, method: method })
|
|
39
|
-
return
|
|
39
|
+
return unless result
|
|
40
40
|
|
|
41
41
|
append_to_activity(context, result)
|
|
42
42
|
return unless blocked?
|
|
@@ -123,10 +123,7 @@ class Contrast::Agent::Protect::Rule::DefaultScanner # rubocop:disable Style/Cla
|
|
|
123
123
|
elsif start_block_comment?(char, index, query)
|
|
124
124
|
boundaries << index
|
|
125
125
|
:STATE_INSIDE_BLOCK_COMMENT
|
|
126
|
-
elsif operator?(char)
|
|
127
|
-
boundaries << index
|
|
128
|
-
:STATE_EXPECTING_TOKEN
|
|
129
|
-
elsif char.match?(Contrast::Utils::ObjectShare::WHITE_SPACE_REGEXP)
|
|
126
|
+
elsif operator?(char) || char.match?(Contrast::Utils::ObjectShare::WHITE_SPACE_REGEXP)
|
|
130
127
|
boundaries << index
|
|
131
128
|
:STATE_EXPECTING_TOKEN
|
|
132
129
|
else
|
|
@@ -17,19 +17,22 @@ module Contrast
|
|
|
17
17
|
BLOCK_MESSAGE = 'Untrusted Deserialization rule triggered. Deserialization blocked.'
|
|
18
18
|
|
|
19
19
|
# Gadgets that map to ERB modules
|
|
20
|
-
ERB_GADGETS = %
|
|
20
|
+
ERB_GADGETS = %W[
|
|
21
21
|
object:ERB
|
|
22
|
+
o:\bERB
|
|
22
23
|
].cs__freeze
|
|
23
24
|
|
|
24
25
|
# Gadgets that map to ActionDispatch modules
|
|
25
26
|
ACTION_DISPATCH_GADGETS = %w[
|
|
26
27
|
object:ActionDispatch::Routing::RouteSet::NamedRouteCollection
|
|
28
|
+
o:\bActionDispatch::Routing::RouteSet::NamedRouteCollection
|
|
27
29
|
].cs__freeze
|
|
28
30
|
|
|
29
31
|
# Gadgets that map to Arel Modules
|
|
30
32
|
AREL_GADGETS = %w[
|
|
31
33
|
string:Arel::Nodes::SqlLiteral
|
|
32
34
|
object:Arel::Nodes
|
|
35
|
+
o:\bArel::Nodes
|
|
33
36
|
].cs__freeze
|
|
34
37
|
|
|
35
38
|
# Used to indicate to TeamServer the gadget is an ERB module
|
|
@@ -21,10 +21,10 @@ module Contrast
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def infilter context, database, query_string
|
|
24
|
-
return
|
|
24
|
+
return unless infilter?(context)
|
|
25
25
|
|
|
26
26
|
result = find_attacker(context, query_string, database: database)
|
|
27
|
-
return
|
|
27
|
+
return unless result
|
|
28
28
|
|
|
29
29
|
append_to_activity(context, result)
|
|
30
30
|
|
|
@@ -36,7 +36,7 @@ module Contrast
|
|
|
36
36
|
|
|
37
37
|
attack_string = input_analysis_result.value
|
|
38
38
|
regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE)
|
|
39
|
-
return
|
|
39
|
+
return unless query_string.match?(regexp)
|
|
40
40
|
|
|
41
41
|
scanner = select_scanner
|
|
42
42
|
ss = StringScanner.new(query_string)
|
|
@@ -22,10 +22,10 @@ module Contrast
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def infilter context, database, query_string
|
|
25
|
-
return
|
|
25
|
+
return unless infilter?(context)
|
|
26
26
|
|
|
27
27
|
result = find_attacker(context, query_string, database: database)
|
|
28
|
-
return
|
|
28
|
+
return unless result
|
|
29
29
|
|
|
30
30
|
append_to_activity(context, result)
|
|
31
31
|
|
|
@@ -36,7 +36,7 @@ module Contrast
|
|
|
36
36
|
attack_string = input_analysis_result.value
|
|
37
37
|
regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE)
|
|
38
38
|
|
|
39
|
-
return
|
|
39
|
+
return unless query_string.match?(regexp)
|
|
40
40
|
|
|
41
41
|
database = kwargs[:database]
|
|
42
42
|
scanner = select_scanner(database)
|
|
@@ -19,7 +19,9 @@ module Contrast
|
|
|
19
19
|
NAME
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
# Given an xml, evaluate it for an XXE attack.
|
|
22
|
+
# Given an xml, evaluate it for an XXE attack. There's no return here
|
|
23
|
+
# as this method handles appending the evaluation to the request
|
|
24
|
+
# context, connecting it to the reporting mechanism at request end.
|
|
23
25
|
#
|
|
24
26
|
# @param context [Contrast::Agent::RequestContext] the context of the
|
|
25
27
|
# request in which this input is evaluated.
|
|
@@ -29,7 +31,7 @@ module Contrast
|
|
|
29
31
|
# attack is found and the rule is in block mode.
|
|
30
32
|
def infilter context, framework, xml
|
|
31
33
|
result = find_attacker(context, xml, framework: framework)
|
|
32
|
-
return
|
|
34
|
+
return unless result
|
|
33
35
|
|
|
34
36
|
append_to_activity(context, result)
|
|
35
37
|
return unless blocked?
|
|
@@ -39,16 +41,23 @@ module Contrast
|
|
|
39
41
|
|
|
40
42
|
protected
|
|
41
43
|
|
|
44
|
+
# Given an XML, find any externally resolved entities and create an
|
|
45
|
+
# Attack Result for them
|
|
46
|
+
#
|
|
47
|
+
# @param context [Contrast::Agent::RequestContext] the context of the
|
|
48
|
+
# request in which this input is evaluated.
|
|
49
|
+
# @param xml [String] the literal value of the XML being checked for
|
|
50
|
+
# external entity resolution
|
|
51
|
+
# @param _kwargs [Hash]
|
|
52
|
+
# @return [Contrast::Api::Dtm::AttackResult, nil] the determination
|
|
53
|
+
# as to whether or not this XML has an XXE attack in it.
|
|
42
54
|
def find_attacker context, xml, **_kwargs
|
|
43
|
-
return
|
|
44
|
-
return
|
|
55
|
+
return unless xml
|
|
56
|
+
return if protect_excluded_by_code?
|
|
45
57
|
|
|
46
|
-
xxe_details
|
|
47
|
-
return
|
|
58
|
+
xxe_details = build_details(xml)
|
|
59
|
+
return unless xxe_details
|
|
48
60
|
|
|
49
|
-
# For our definition, the prolog goes from the start of the XML
|
|
50
|
-
# string to the end of the last entity declaration.
|
|
51
|
-
xxe_details.xml = Contrast::Utils::StringUtils.protobuf_safe_string(xml[0, last_idx])
|
|
52
61
|
ia_result = build_evaluation(xxe_details.xml)
|
|
53
62
|
build_attack_with_match(
|
|
54
63
|
context,
|
|
@@ -58,7 +67,15 @@ module Contrast
|
|
|
58
67
|
details: xxe_details)
|
|
59
68
|
end
|
|
60
69
|
|
|
61
|
-
|
|
70
|
+
# Given an XML determined to be unsafe, build out the details of the
|
|
71
|
+
# attack. The details will include a substring of the given XML up to
|
|
72
|
+
# the end of the prolog, where the external entities are declared.
|
|
73
|
+
#
|
|
74
|
+
# @param xml [String] the literal value of the XML being checked for
|
|
75
|
+
# external entity resolution
|
|
76
|
+
# @return [Contrast::Api::Dtm::XxeDetails] The details of
|
|
77
|
+
# the XXE attack and the index of the last entity discovered
|
|
78
|
+
def build_details xml
|
|
62
79
|
last_idx = 0
|
|
63
80
|
ss = StringScanner.new(xml)
|
|
64
81
|
while ss.scan_until(EXTERNAL_ENTITY_PATTERN)
|
|
@@ -70,7 +87,11 @@ module Contrast
|
|
|
70
87
|
xxe_details.declared_entities << build_match(ss)
|
|
71
88
|
xxe_details.entities_resolved << build_wrapper(entity_wrapper)
|
|
72
89
|
end
|
|
73
|
-
|
|
90
|
+
# For our definition, the prolog goes from the start of the XML
|
|
91
|
+
# string to the end of the last entity declaration.
|
|
92
|
+
xxe_details.xml = Contrast::Utils::StringUtils.protobuf_safe_string(xml[0, last_idx]) if xxe_details
|
|
93
|
+
|
|
94
|
+
xxe_details
|
|
74
95
|
end
|
|
75
96
|
|
|
76
97
|
def build_sample context, ia_result, _url, **kwargs
|
|
@@ -18,12 +18,16 @@ module Contrast
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def external_entity?
|
|
21
|
-
@_external_entity
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
if @_external_entity.nil?
|
|
22
|
+
@_external_entity ||= if @system_id
|
|
23
|
+
external_id?(@system_id)
|
|
24
|
+
elsif @public_id
|
|
25
|
+
external_id?(@public_id)
|
|
26
|
+
else
|
|
27
|
+
false
|
|
28
|
+
end
|
|
26
29
|
end
|
|
30
|
+
@_external_entity
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
# <!ENTITY name SYSTEM "URI">
|
|
@@ -48,7 +52,7 @@ module Contrast
|
|
|
48
52
|
UP_DIR_LINUX = '../'
|
|
49
53
|
UP_DIR_WIN = '..\\'
|
|
50
54
|
# we only use this against lowercase strings, removed A-Z for speed
|
|
51
|
-
FILE_PATTERN_WINDOWS =
|
|
55
|
+
FILE_PATTERN_WINDOWS = /^\\*[a-z]{1,3}:.*/.cs__freeze
|
|
52
56
|
def external_id? entity_id
|
|
53
57
|
return false unless entity_id
|
|
54
58
|
|
|
@@ -22,7 +22,7 @@ module Contrast
|
|
|
22
22
|
# @param application_settings [Contrast::Api::Settings::ApplicationSettings]
|
|
23
23
|
# those settings which the Service has relayed from TeamServer.
|
|
24
24
|
def self.process application_settings
|
|
25
|
-
return
|
|
25
|
+
return unless application_settings&.reactions&.any?
|
|
26
26
|
|
|
27
27
|
application_settings.reactions.each do |reaction|
|
|
28
28
|
# the enums are all uppercase, we need to downcase them before attempting to log
|
|
@@ -118,16 +118,16 @@ module Contrast
|
|
|
118
118
|
# holds, wraps, or is the body of the Response
|
|
119
119
|
# @return [nil, String] the content of the body
|
|
120
120
|
def extract_body body
|
|
121
|
-
return
|
|
121
|
+
return unless body
|
|
122
122
|
|
|
123
123
|
if defined?(Rack::File) && body.is_a?(Rack::File)
|
|
124
124
|
# not sure what to do in this situation, so don't do anything.
|
|
125
125
|
nil
|
|
126
126
|
elsif body.is_a?(Rack::BodyProxy)
|
|
127
127
|
handle_rack_body_proxy(body)
|
|
128
|
-
elsif defined?(ActionDispatch::Response::RackBody) && body.is_a?(ActionDispatch::Response::RackBody)
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
elsif (defined?(ActionDispatch::Response::RackBody) && body.is_a?(ActionDispatch::Response::RackBody)) ||
|
|
129
|
+
body.is_a?(Rack::Response)
|
|
130
|
+
|
|
131
131
|
extract_body(body.body)
|
|
132
132
|
elsif Contrast::Utils::DuckUtils.quacks_to?(body, :each)
|
|
133
133
|
acc = []
|
|
@@ -152,7 +152,7 @@ module Contrast
|
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
def read_or_string obj
|
|
155
|
-
return
|
|
155
|
+
return unless obj
|
|
156
156
|
|
|
157
157
|
if Contrast::Utils::DuckUtils.quacks_to?(obj, :read)
|
|
158
158
|
tmp = obj.read
|