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.
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