Module: Contrast::Agent::Patching::Policy::Patch

Extended by:
Assess::Policy::PropagationMethod, Assess::Policy::SourceMethod, Assess::Policy::TriggerMethod, Components::Interface
Defined in:
lib/contrast/agent/patching/policy/patch.rb,
ext/cs__common/cs__common.c

Overview

This is how we patch into our customer’s code. It provides a way to track which classes we need to patch into and, once we’ve woven, provides a map for which methods our renamed functions need to call and how.

Constant Summary collapse

POLICIES =
[
  Contrast::Agent::Assess::Policy::Policy,
  Contrast::Agent::Inventory::Policy::Policy,
  Contrast::Agent::Protect::Policy::Policy
].cs__freeze

Constants included from Assess::Policy::TriggerMethod

Assess::Policy::TriggerMethod::CURRENT_FINDING_VERSION, Assess::Policy::TriggerMethod::MINIMUM_FINDING_VERSION

Constants included from Assess::Policy::PropagationMethod

Assess::Policy::PropagationMethod::APPEND_ACTION, Assess::Policy::PropagationMethod::CENTER_ACTION, Assess::Policy::PropagationMethod::CUSTOM_ACTION, Assess::Policy::PropagationMethod::DB_WRITE_ACTION, Assess::Policy::PropagationMethod::INSERT_ACTION, Assess::Policy::PropagationMethod::KEEP_ACTION, Assess::Policy::PropagationMethod::NEXT_ACTION, Assess::Policy::PropagationMethod::NOOP_ACTION, Assess::Policy::PropagationMethod::PREPEND_ACTION, Assess::Policy::PropagationMethod::PROPAGATION_ACTIONS, Assess::Policy::PropagationMethod::REMOVE_ACTION, Assess::Policy::PropagationMethod::REPLACE_ACTION, Assess::Policy::PropagationMethod::REVERSE_ACTION, Assess::Policy::PropagationMethod::SPLAT_ACTION, Assess::Policy::PropagationMethod::SPLIT_ACTION, Assess::Policy::PropagationMethod::ZERO_LENGTH_ACTIONS

Constants included from Assess::Policy::SourceMethod

Assess::Policy::SourceMethod::COOKIE_KEY_TYPE, Assess::Policy::SourceMethod::COOKIE_TYPE, Assess::Policy::SourceMethod::HEADER_KEY_TYPE, Assess::Policy::SourceMethod::HEADER_TYPE, Assess::Policy::SourceMethod::PARAMETER_KEY_TYPE, Assess::Policy::SourceMethod::PARAMETER_TYPE

Class Method Summary collapse

Methods included from Components::Interface

included

Methods included from Assess::Policy::TriggerMethod

apply_eval_trigger, apply_trigger_rule, build_finding, report_finding

Methods included from Assess::Policy::PropagationMethod

apply_propagation, apply_propagator, apply_tags, apply_untags, appropriate_target?, can_propagate?, context_available?, determine_target, valid_length?, valid_target?

Methods included from Assess::Policy::SourceMethod

source_patchers

Class Method Details

.apply_assess(method_policy, preshift, object, ret, args, block) ⇒ Object

Apply the Assess patches which apply to the given method.

Parameters:

  • method_policy (Contrast::Agent::Patching::Policy::MethodPolicy)

    Mapping of the triggers on the given method.

  • preshift (Contrast::Agent::Assess::PreShift)

    The capture of the state of the code just prior to the invocation of the patched method.

  • object (Object)

    The object on which the method was invoked, typically what would be returned by self.

  • ret (Object)

    The return of the method that was invoked.

  • args (Array<Object>)

    The arguments passed to the method being invoked.

  • block (Proc)

    The block passed to the method that was invoked.



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/contrast/agent/patching/policy/patch.rb', line 175

def apply_assess method_policy, preshift, object, ret, args, block
  source_ret = nil
  propagated_ret = nil
  return ret unless method_policy && ASSESS.enabled?

  current_context = Contrast::Agent::REQUEST_TRACKER.current
  return ret unless current_context.analyze_request?

  trigger_node = method_policy.trigger_node
  Contrast::Agent::Assess::Policy::TriggerMethod.apply_trigger_rule(trigger_node, object, ret, args) if trigger_node
  if method_policy.source_node
    # If we were given a frozen return, and it was the target of a
    # source, and we have frozen sources enabled, we'll need to
    # replace the return. Note, this is not the default case.
    source_ret = Contrast::Agent::Assess::Policy::SourceMethod.source_patchers(method_policy, object, ret, args)
  end
  if method_policy.propagation_node
    propagated_ret = Contrast::Agent::Assess::Policy::PropagationMethod.apply_propagation(
        method_policy,
        preshift,
        object,
        source_ret || ret,
        args,
        block)
  end
  handle_return(propagated_ret, source_ret, ret)
rescue StandardError => e
  logger.error('Unable to assess method call.', e)
  handle_return(propagated_ret, source_ret, ret)
rescue Exception => e # rubocop:disable Lint/RescueException
  logger.error('Unable to assess method call.', e)
  handle_return(propagated_ret, source_ret, ret)
  raise e
end

.apply_inventory(method_policy, method, exception, object, args) ⇒ Object

Apply the Inventory patch which applies to the given method.

Parameters:

  • method_policy (Contrast::Agent::Patching::Policy::MethodPolicy)

    Mapping of the triggers on the given method.

  • method (Symbol)

    The method into which we’re patching

  • exception (StandardError)

    Any exception raised during the call of the patched method.

  • object (Object)

    The object on which the method is invoked, typically what would be returned by self.

  • args (Array<Object>)

    The arguments passed to the method being invoked.



151
152
153
154
155
156
157
158
159
# File 'lib/contrast/agent/patching/policy/patch.rb', line 151

def apply_inventory method_policy, method, exception, object, args
  return unless INVENTORY.enabled?

  apply_trigger_only(method_policy&.inventory_node,
                     method,
                     exception,
                     object,
                     args)
end

.apply_post_patch(method_policy, preshift, object, ret, args, block) ⇒ Object

THIS IS CALLED FROM C. Do not change the signature lightly.

This method functions to call the infilter methods from our patches, allowing for analysis and reporting at the point just after the patched code is invoked

Parameters:

  • method_policy (Contrast::Agent::Patching::Policy::MethodPolicy)

    Mapping of the triggers on the given method.

  • preshift (Contrast::Agent::Assess::PreShift)

    The capture of the state of the code just prior to the invocation of the patched method.

  • object (Object)

    The object on which the method was invoked, typically what would be returned by self.

  • ret (Object)

    The return of the method that was invoked.

  • args (Array<Object>)

    The arguments passed to the method being invoked.

  • block (Proc)

    The block passed to the method that was invoked.



112
113
114
115
116
# File 'lib/contrast/agent/patching/policy/patch.rb', line 112

def apply_post_patch method_policy, preshift, object, ret, args, block
  apply_assess(method_policy, preshift, object, ret, args, block)
rescue StandardError => e
  logger.error('Unable to apply post patch to method.', e)
end

.apply_pre_patch(method_policy, method, exception, object, args) ⇒ Object

THIS IS CALLED FROM C. Do not change the signature lightly.

This method functions to call the infilter methods from our patches, allowing for analysis and reporting at the point just before the patched code is invoked.

Parameters:

  • method_policy (Contrast::Agent::Patching::Policy::MethodPolicy)

    Mapping of the triggers on the given method.

  • method (Symbol)

    The method into which we’re patching

  • exception (StandardError)

    Any exception raised during the call of the patched method.

  • object (Object)

    The object on which the method is invoked, typically what would be returned by self.

  • args (Array<Object>)

    The arguments passed to the method being invoked.



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/contrast/agent/patching/policy/patch.rb', line 75

def apply_pre_patch method_policy, method, exception, object, args
  apply_protect(method_policy, method, exception, object, args)
  apply_inventory(method_policy, method, exception, object, args)
rescue Contrast::SecurityException => e
  # We were told to block something, so we gotta. Don't catch this
  # one, let it get back to our Middleware or even all the way out to
  # the framework
  raise e
rescue StandardError => e
  # Anything else was our bad and we gotta catch that to allow for
  # normal application flow
  logger.error('Unable to apply pre patch to method.', e)
rescue Exception => e # rubocop:disable Lint/RescueException
  # This is something like NoMemoryError that we can't
  # hope to handle.  Nonetheless, shouldn't leak scope.
  exit_contrast_scope!
  raise e
end

.apply_protect(method_policy, method, exception, object, args) ⇒ Object

Apply the Protect patch which applies to the given method.

Parameters:

  • method_policy (Contrast::Agent::Patching::Policy::MethodPolicy)

    Mapping of the triggers on the given method.

  • method (Symbol)

    The method into which we’re patching

  • exception (StandardError)

    Any exception raised during the call of the patched method.

  • object (Object)

    The object on which the method is invoked, typically what would be returned by self.

  • args (Array<Object>)

    The arguments passed to the method being invoked.



129
130
131
132
133
134
135
136
137
138
# File 'lib/contrast/agent/patching/policy/patch.rb', line 129

def apply_protect method_policy, method, exception, object, args
  return unless AGENT.enabled?
  return unless PROTECT.enabled?

  apply_trigger_only(method_policy&.protect_node,
                     method,
                     exception,
                     object,
                     args)
end

.apply_trigger_only(trigger_node, method, exception, object, args) ⇒ Object

Generic invocation of the Inventory or Protect patch which apply to the given method.

Parameters:

  • trigger_node (Contrast::Agent::Inventory::Policy::TriggerNode)

    Mapping of the specific trigger on the given method.

  • method (Symbol)

    The method into which we’re patching

  • exception (StandardError)

    Any exception raised during the call of the patched method.

  • object (Object)

    The object on which the method is invoked, typically what would be returned by self.

  • args (Array<Object>)

    The arguments passed to the method being invoked.



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/contrast/agent/patching/policy/patch.rb', line 222

def apply_trigger_only trigger_node, method, exception, object, args
  return unless trigger_node

  # If that rule only applies in the case of an exception being
  # thrown and there's no exception here, move along, or vice versa
  return if trigger_node.on_exception && !exception
  return if !trigger_node.on_exception && exception

  # Each patch has an applicator that handles logic for it. Think
  # of this as being similar to propagator actions, most closely
  # resembling CUSTOM - they all have a common interface but their
  # own logic based on what's in the method(s) they've been patched
  # into.
  # Each patch also knows the method of its applicator. Some
  # things, like AppliesXxeRule, have different methods depending
  # on the library patched. This lets us handle the boilerplate of
  # patching while still allowing for custom handling of the
  # methods.
  applicator_method = trigger_node.applicator_method
  # By calling send like this, we can reuse all the patching.
  # We `send` to the given method of the given class
  # (applicator) since they all accept the same inputs
  trigger_node.applicator.send(applicator_method, method, exception, trigger_node.properties, object, args)
end

.build_method_name(patched_class, patched_method) ⇒ Symbol

Given a module and method, construct an expected name for the alias by which Contrast will reference the original.

Parameters:

  • patched_class (Module)

    the module being patched

  • patched_method (Symbol)

    the method being patched

Returns:

  • (Symbol)


269
270
271
272
273
274
# File 'lib/contrast/agent/patching/policy/patch.rb', line 269

def build_method_name patched_class, patched_method
  (Contrast::Utils::ObjectShare::CONTRAST_PATCHED_METHOD_START +
      patched_class.cs__name.gsub('::', '_').downcase +
      Contrast::Utils::ObjectShare::UNDERSCORE +
      patched_method.to_s).to_sym
end

.build_unbound_method_name(patcher_method) ⇒ Object

Given a method, return a symbol in the format <method_start>unbound<method_name>



278
279
280
281
282
283
# File 'lib/contrast/agent/patching/policy/patch.rb', line 278

def build_unbound_method_name patcher_method
  (Contrast::Utils::ObjectShare::CONTRAST_PATCHED_METHOD_START +
      'unbound' +
      Contrast::Utils::ObjectShare::UNDERSCORE +
      patcher_method.to_s).to_sym
end

.enter_method_scope!(method_policy) ⇒ Object



48
49
50
51
52
# File 'lib/contrast/agent/patching/policy/patch.rb', line 48

def enter_method_scope! method_policy
  method_policy.scopes_to_enter.each do |scope|
    enter_scope!(scope)
  end
end

.exit_method_scope!(method_policy) ⇒ Object



54
55
56
57
58
# File 'lib/contrast/agent/patching/policy/patch.rb', line 54

def exit_method_scope! method_policy
  method_policy.scopes_to_exit.each do |scope|
    exit_scope!(scope)
  end
end

.handle_return(propagated_ret, source_ret, ret) ⇒ Object?

Method to choose which replaced return from the post_patch to actually return

Parameters:

  • propagated_ret (Object, nil)

    The replaced return from the propagation patch.

  • source_ret (Object, nil)

    The replaced return from the source patch.

  • ret (Object, nil)

    The original return of the patched method.

Returns:

  • (Object, nil)

    The thing to return from the post patch.



257
258
259
260
261
# File 'lib/contrast/agent/patching/policy/patch.rb', line 257

def handle_return propagated_ret, source_ret, ret
  safe_return = propagated_ret || source_ret || ret
  safe_return.rewind if Contrast::Utils::IOUtil.should_rewind?(safe_return)
  safe_return
end

.instrument_with_alias(mod, methods, method_policy) ⇒ Boolean

Returns if patched, either by this invocation or a previous, or not.

Parameters:

  • mod (Module)

    the module in which the patch should be placed.

  • methods (Array(Symbol))

    all the instance or singleton methods in this clazz.

  • method_policy (Contrast::Agent::Patching::Policy::MethodPolicy)

    the policy that applies to the method to be patched.

Returns:

  • (Boolean)

    if patched, either by this invocation or a previous, or not



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/contrast/agent/patching/policy/patch.rb', line 293

def instrument_with_alias mod, methods, method_policy
  cs_method_name = build_method_name(mod, method_policy.method_name)
  # we've already patched this class, don't do it again
  return true if methods.include?(cs_method_name)

  begin
    contrast_define_method(mod, method_policy, cs_method_name)
  rescue NameError => e
    # This shouldn't happen anymore, but just in case calling alias
    # results in a NameError, we'll be safe here.
    logger.error(
        'Attempted to alias a method on a Module that doesn\'t respond to it.',
        e,
        module: mod.cs__name,
        method: method_policy.method_name)
    return false
  end
  true
end

.instrument_with_prepend(mod, method_policy) ⇒ Boolean

Returns if patched, either by this invocation or a previous, or not.

Parameters:

Returns:

  • (Boolean)

    if patched, either by this invocation or a previous, or not



319
320
321
# File 'lib/contrast/agent/patching/policy/patch.rb', line 319

def instrument_with_prepend mod, method_policy
  contrast_prepend_method(mod, method_policy)
end

.register_c_hook(unbound_method) ⇒ Object

Parameters:

  • unbound_method (UnboundMethod)

    An unbound method, that doesn’t reference its binding. This method executes C hooking code.



325
326
327
328
# File 'lib/contrast/agent/patching/policy/patch.rb', line 325

def register_c_hook unbound_method
  # current binding is as meaningless as any other.  but we need something
  unbound_method.bind_call(self)
end

.register_c_patch(target_module_name, unbound_method, impl = :alias_instance) ⇒ Symbol

Returns new alias for the underlying method (presumably, so the patched method can call it).

Parameters:

  • target_module_name (String)

    Fully-qualified module name, as string, to which the C patch applies.

  • unbound_method (UnboundMethod)

    An unbound method, to be patched into target_module.

  • impl (Symbol) (defaults to: :alias_instance)

    Strategy for applying the patch: { :alias_instance, :alias_singleton, or :prepend }: :alias_instance -> alias instance method of module :alias_singleton -> alias instance method of singleton class of module

    (equivalent to :alias, where `module = module.singleton class`)
    (this is a.k.a. "class-method patch")
    

    :prepend -> prepend instance method of module

    prepending singleton is easily supported too, just not implemented yet.

Returns:

  • (Symbol)

    new alias for the underlying method (presumably, so the patched method can call it)



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/contrast/agent/patching/policy/patch.rb', line 340

def register_c_patch target_module_name, unbound_method, impl = :alias_instance
  # These could be set as AfterLoadPatches.
  method_name = unbound_method.name.to_sym
  underlying_method_name = build_unbound_method_name(method_name).to_sym

  target_module = Module.cs__const_get(target_module_name)

  target_module = target_module.cs__singleton_class if %i[alias_singleton prepend].include? impl

  visibility = if target_module.private_instance_methods(false).include?(method_name)
                 :private
               elsif target_module.protected_instance_methods(false).include?(method_name)
                 :protected
               elsif target_module.public_instance_methods(false).include?(method_name)
                 :public
               else
                 raise NoMethodError,
                       <<~ERR
                         Tried to register a C-defined #{ impl } patch for \
                         #{ target_module_name }##{ method_name }, but can't find :#{ method_name }.
                       ERR
               end

  case impl
  when :alias_instance, :alias_singleton
    unless target_module.instance_methods(false).include? underlying_method_name
      # alias_method may be private
      target_module.send(:alias_method, underlying_method_name, method_name)
      # TODO: RUBY-1052
      # rubocop:disable Kernel/DefineMethod
      target_module.send(:define_method, method_name, unbound_method.bind(target_module))
      # rubocop:enable Kernel/DefineMethod
    end
    target_module.send(visibility, method_name) # e.g., module.private(:my_method)
  when :prepend
    prepending_module = Module.new
    # TODO: RUBY-1052
    # rubocop:disable Kernel/DefineMethod
    prepending_module.send(:define_method, method_name, unbound_method.bind(target_module))
    # rubocop:enable Kernel/DefineMethod
    prepending_module.send(visibility, method_name)
    # This prepends to the singleton class (it patches a class method)
    target_module.prepend prepending_module
  end
  # Ougai::Logger.create_item_with_2args calls Hash#[]=, so we
  # can't invoke this logging method or we'll seg fault as we'd
  # change the method definition mid-call
  # if method_name != :[]=
  #   logger.trace(
  #       'Registered C-defined patch',
  #       implementation: impl,
  #       module: target_module_name,
  #       method: method_name,
  #       visibility: visibility)
  # end
  underlying_method_name.to_sym
end

.skip_assess_analysis?Boolean

Skip if we should skip_contrast_analysis?, sampling says to ignore this request, or assess has been disabled.

Returns:

  • (Boolean)


411
412
413
414
415
# File 'lib/contrast/agent/patching/policy/patch.rb', line 411

def skip_assess_analysis?
  return true if skip_contrast_analysis?

  !ASSESS.enabled?
end

.skip_contrast_analysis?Boolean

Returns:

  • (Boolean)


399
400
401
402
403
404
405
# File 'lib/contrast/agent/patching/policy/patch.rb', line 399

def skip_contrast_analysis?
  return true if in_contrast_scope?
  return true unless defined?(Contrast::Agent::REQUEST_TRACKER)
  return true unless Contrast::Agent::REQUEST_TRACKER.current&.analyze_request?

  false
end