# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true module Contrast module Utils module Patching # This module will include all methods for different patch applies from Patch module and some other module # methods from the same place, so we can ease the main module module PatchUtils # Method to choose which replaced return from the post_patch to actually return. # # @param propagated_ret [Object, nil] The replaced return from the propagation patch. # @param source_ret [Object, nil] The replaced return from the source patch. # @param ret [Object, nil] The original return of the patched method. # @return [Object, nil] The thing to return from the post patch. 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 # Given a module and method, construct an expected name for the alias by which Contrast will reference the # original. # # @param patched_class [Module] the module being patched # @param patched_method [Symbol] the method being patched # @return [Symbol] 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 # Given a method, return a symbol in the format # _unbound_ def build_unbound_method_name patcher_method "#{ Contrast::Utils::ObjectShare::CONTRAST_PATCHED_METHOD_START }unbound"\ "#{ Contrast::Utils::ObjectShare::UNDERSCORE }"\ "#{ patcher_method }".to_sym end # ===== PATCH APPLIERS ===== # 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. # # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] Mapping of the triggers on the given # method. # @param method [Symbol] The method into which we're patching # @param exception [StandardError] Any exception raised during the call of the patched method. # @param object [Object] The object on which the method is invoked, typically what would be returned by self. # @param args [Array] The arguments passed to the method being invoked. 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) end # 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 # # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] Mapping of the triggers on the given # method. # @param preshift [Contrast::Agent::Assess::PreShift] The capture of the state of the code just prior to the # invocation of the patched method. # @param object [Object] The object on which the method was invoked, typically what would be returned by self. # @param ret [Object] The return of the method that was invoked. # @param args [Array] The arguments passed to the method being invoked. # @param block [Proc] The block passed to the method that was invoked. 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 the Protect patch which applies to the given method. # # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] Mapping of the triggers on the given # method. # @param method [Symbol] The method into which we're patching # @param exception [StandardError] Any exception raised during the call of the patched method. # @param object [Object] The object on which the method is invoked, typically what would be returned by self. # @param args [Array] The arguments passed to the method being invoked. def apply_protect method_policy, method, exception, object, args return unless ::Contrast::AGENT.enabled? return unless ::Contrast::PROTECT.enabled? apply_trigger_only(method_policy&.protect_node, method, exception, object, args) end # Apply the Inventory patch which applies to the given method. # # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] Mapping of the triggers on the given # method. # @param method [Symbol] The method into which we're patching # @param exception [StandardError] Any exception raised during the call of the patched method. # @param object [Object] The object on which the method is invoked, typically what would be returned by self. # @param args [Array] The arguments passed to the method being invoked. def apply_inventory method_policy, method, exception, object, args return unless ::Contrast::INVENTORY.enabled? apply_trigger_only(method_policy&.inventory_node, method, exception, object, args) end # Apply the Assess patches which apply to the given method. # # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] Mapping of the triggers on the given # method. # @param preshift [Contrast::Agent::Assess::PreShift] The capture of the state of the code just prior to the # invocation of the patched method. # @param object [Object] The object on which the method was invoked, typically what would be returned by self. # @param ret [Object] The return of the method that was invoked. # @param args [Array] The arguments passed to the method being invoked. # @param block [Proc] The block passed to the method that was invoked. def apply_assess method_policy, preshift, object, ret, args, block source_ret = nil propagated_ret = nil return ret unless method_policy && ::Contrast::ASSESS.enabled? current_context = Contrast::Agent::REQUEST_TRACKER.current return ret if current_context && !current_context.analyze_request? trigger_node = method_policy.trigger_node if trigger_node Contrast::Agent::Assess::Policy::TriggerMethod.apply_trigger_rule(trigger_node, object, ret, args) end 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.apply_source(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 # Generic invocation of the Inventory or Protect patch which apply to the given method. # # @param trigger_node [Contrast::Agent::Inventory::Policy::TriggerNode] Mapping of the specific trigger on the # given method. # @param method [Symbol] The method into which we're patching # @param exception [StandardError] Any exception raised during the call of the patched method. # @param object [Object] The object on which the method is invoked, typically what would be returned by self. # @param args [Array] The arguments passed to the method being invoked. 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 end end end end