contrast-agent 4.2.0 → 4.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -0
  3. data/ext/cs__assess_marshal_module/cs__assess_marshal_module.c +22 -10
  4. data/ext/cs__assess_marshal_module/cs__assess_marshal_module.h +4 -3
  5. data/lib/contrast/agent/assess/contrast_event.rb +49 -130
  6. data/lib/contrast/agent/assess/contrast_object.rb +51 -0
  7. data/lib/contrast/agent/assess/events/source_event.rb +4 -9
  8. data/lib/contrast/agent/assess/policy/patcher.rb +4 -3
  9. data/lib/contrast/agent/assess/policy/policy_node.rb +31 -59
  10. data/lib/contrast/agent/assess/policy/preshift.rb +3 -3
  11. data/lib/contrast/agent/assess/policy/propagation_method.rb +13 -19
  12. data/lib/contrast/agent/assess/policy/propagation_node.rb +12 -24
  13. data/lib/contrast/agent/assess/policy/propagator/append.rb +1 -2
  14. data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -2
  15. data/lib/contrast/agent/assess/policy/propagator/custom.rb +1 -1
  16. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +1 -3
  17. data/lib/contrast/agent/assess/policy/propagator/insert.rb +1 -2
  18. data/lib/contrast/agent/assess/policy/propagator/keep.rb +1 -2
  19. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +3 -2
  20. data/lib/contrast/agent/assess/policy/propagator/next.rb +1 -2
  21. data/lib/contrast/agent/assess/policy/propagator/prepend.rb +1 -2
  22. data/lib/contrast/agent/assess/policy/propagator/remove.rb +2 -4
  23. data/lib/contrast/agent/assess/policy/propagator/replace.rb +1 -2
  24. data/lib/contrast/agent/assess/policy/propagator/reverse.rb +1 -2
  25. data/lib/contrast/agent/assess/policy/propagator/select.rb +3 -4
  26. data/lib/contrast/agent/assess/policy/propagator/splat.rb +2 -4
  27. data/lib/contrast/agent/assess/policy/propagator/split.rb +73 -117
  28. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +11 -11
  29. data/lib/contrast/agent/assess/policy/propagator/trim.rb +3 -7
  30. data/lib/contrast/agent/assess/policy/source_method.rb +2 -14
  31. data/lib/contrast/agent/assess/policy/trigger/reflected_xss.rb +5 -8
  32. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +1 -1
  33. data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +1 -1
  34. data/lib/contrast/agent/assess/property/tagged.rb +21 -15
  35. data/lib/contrast/agent/assess/rule/redos.rb +1 -1
  36. data/lib/contrast/agent/assess/tracker.rb +16 -18
  37. data/lib/contrast/agent/deadzone/policy/deadzone_node.rb +7 -0
  38. data/lib/contrast/agent/middleware.rb +50 -1
  39. data/lib/contrast/agent/patching/policy/method_policy.rb +1 -1
  40. data/lib/contrast/agent/patching/policy/patch.rb +4 -4
  41. data/lib/contrast/agent/protect/policy/applies_deserialization_rule.rb +47 -1
  42. data/lib/contrast/agent/protect/policy/rule_applicator.rb +53 -0
  43. data/lib/contrast/agent/protect/rule/base.rb +63 -14
  44. data/lib/contrast/agent/protect/rule/cmd_injection.rb +3 -3
  45. data/lib/contrast/agent/protect/rule/default_scanner.rb +1 -4
  46. data/lib/contrast/agent/protect/rule/deserialization.rb +4 -1
  47. data/lib/contrast/agent/protect/rule/no_sqli.rb +3 -3
  48. data/lib/contrast/agent/protect/rule/sqli.rb +3 -3
  49. data/lib/contrast/agent/protect/rule/xxe.rb +32 -11
  50. data/lib/contrast/agent/protect/rule/xxe/entity_wrapper.rb +10 -6
  51. data/lib/contrast/agent/reaction_processor.rb +1 -1
  52. data/lib/contrast/agent/response.rb +5 -5
  53. data/lib/contrast/agent/rewriter.rb +3 -3
  54. data/lib/contrast/agent/scope.rb +33 -13
  55. data/lib/contrast/agent/static_analysis.rb +13 -7
  56. data/lib/contrast/agent/version.rb +1 -1
  57. data/lib/contrast/api/decorators/library.rb +1 -0
  58. data/lib/contrast/api/decorators/library_usage_update.rb +1 -0
  59. data/lib/contrast/api/decorators/trace_event.rb +19 -31
  60. data/lib/contrast/api/decorators/trace_event_object.rb +11 -3
  61. data/lib/contrast/api/decorators/trace_event_signature.rb +27 -5
  62. data/lib/contrast/api/decorators/user_input.rb +2 -1
  63. data/lib/contrast/common_agent_configuration.rb +1 -1
  64. data/lib/contrast/components/assess.rb +36 -0
  65. data/lib/contrast/components/interface.rb +5 -3
  66. data/lib/contrast/components/scope.rb +23 -0
  67. data/lib/contrast/components/settings.rb +3 -3
  68. data/lib/contrast/config/assess_configuration.rb +2 -1
  69. data/lib/contrast/extension/assess/array.rb +1 -2
  70. data/lib/contrast/extension/assess/erb.rb +1 -3
  71. data/lib/contrast/extension/assess/exec_trigger.rb +1 -1
  72. data/lib/contrast/extension/assess/fiber.rb +2 -3
  73. data/lib/contrast/extension/assess/hash.rb +4 -2
  74. data/lib/contrast/extension/assess/kernel.rb +1 -2
  75. data/lib/contrast/extension/assess/marshal.rb +34 -26
  76. data/lib/contrast/extension/assess/regexp.rb +3 -8
  77. data/lib/contrast/extension/assess/string.rb +1 -2
  78. data/lib/contrast/framework/base_support.rb +51 -53
  79. data/lib/contrast/framework/manager.rb +3 -2
  80. data/lib/contrast/framework/rack/patch/session_cookie.rb +1 -1
  81. data/lib/contrast/framework/rack/support.rb +2 -1
  82. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +1 -1
  83. data/lib/contrast/framework/rails/patch/rails_application_configuration.rb +1 -1
  84. data/lib/contrast/framework/rails/rewrite/action_controller_railties_helper_inherited.rb +1 -1
  85. data/lib/contrast/framework/rails/rewrite/active_record_attribute_methods_read.rb +1 -1
  86. data/lib/contrast/framework/rails/rewrite/active_record_time_zone_inherited.rb +1 -1
  87. data/lib/contrast/framework/rails/support.rb +2 -1
  88. data/lib/contrast/framework/sinatra/support.rb +3 -2
  89. data/lib/contrast/logger/application.rb +0 -3
  90. data/lib/contrast/utils/duck_utils.rb +1 -1
  91. data/lib/contrast/utils/heap_dump_util.rb +1 -1
  92. data/lib/contrast/utils/object_share.rb +3 -3
  93. data/lib/contrast/utils/preflight_util.rb +1 -1
  94. data/lib/contrast/utils/prevent_serialization.rb +1 -1
  95. data/lib/contrast/utils/resource_loader.rb +1 -1
  96. data/lib/contrast/utils/sha256_builder.rb +2 -2
  97. data/lib/contrast/utils/string_utils.rb +1 -1
  98. data/lib/contrast/utils/tag_util.rb +9 -13
  99. data/resources/assess/policy.json +9 -9
  100. data/resources/deadzone/policy.json +156 -0
  101. data/resources/protect/policy.json +12 -0
  102. data/ruby-agent.gemspec +9 -6
  103. data/service_executables/VERSION +1 -1
  104. data/service_executables/linux/contrast-service +0 -0
  105. data/service_executables/mac/contrast-service +0 -0
  106. 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} and {#build_details} to implement
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([Contrast::Api::Settings::ProtectionRule::Mode::BLOCK,
24
- Contrast::Api::Settings::ProtectionRule::Mode::BLOCK_AT_PERIMETER]).cs__freeze
25
- POSTFILTER_MODES = Set.new([Contrast::Api::Settings::ProtectionRule::Mode::BLOCK,
26
- Contrast::Api::Settings::ProtectionRule::Mode::PERMIT,
27
- Contrast::Api::Settings::ProtectionRule::Mode::MONITOR]).cs__freeze
28
- STACK_COLLECTION_RESULTS = Set.new([Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED,
29
- Contrast::Api::Dtm::AttackResult::ResponseType::MONITORED]).cs__freeze
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 nil unless result
278
+ return unless result
230
279
 
231
280
  sample = build_sample(context, ia_result, candidate_string, **kwargs)
232
- return nil unless sample
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 nil unless infilter?(context)
26
+ return unless infilter?(context)
27
27
 
28
28
  ia_results = gather_ia_results(context)
29
- return nil if ia_results.empty?
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 nil unless result
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 = %w[
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 nil unless infilter?(context)
24
+ return unless infilter?(context)
25
25
 
26
26
  result = find_attacker(context, query_string, database: database)
27
- return nil unless result
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 nil unless query_string.match?(regexp)
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 nil unless infilter?(context)
25
+ return unless infilter?(context)
26
26
 
27
27
  result = find_attacker(context, query_string, database: database)
28
- return nil unless result
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 nil unless query_string.match?(regexp)
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 nil unless result
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 nil unless xml
44
- return nil if protect_excluded_by_code?
55
+ return unless xml
56
+ return if protect_excluded_by_code?
45
57
 
46
- xxe_details, last_idx = build_details(xml)
47
- return nil unless xxe_details
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
- def build_details xml, _evaluation = nil
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
- [xxe_details, last_idx]
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 ||= begin
22
- return external_id?(@system_id) if @system_id
23
- return external_id?(@public_id) if @public_id
24
-
25
- false
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 = /^[\\\\]*[a-z]{1,3}:.*/.cs__freeze
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 nil unless application_settings&.reactions&.any?
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 nil unless body
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
- extract_body(body.body)
130
- elsif body.is_a?(Rack::Response)
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 nil unless obj
155
+ return unless obj
156
156
 
157
157
  if Contrast::Utils::DuckUtils.quacks_to?(obj, :read)
158
158
  tmp = obj.read