# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true cs__scoped_require 'contrast/utils/object_share' cs__scoped_require 'contrast/utils/sha256_builder' cs__scoped_require 'contrast/agent/assess/policy/trigger_validation/trigger_validation' cs__scoped_require 'contrast/components/interface' module Contrast module Agent module Assess module Policy # A trigger method is one which can perform a dangerous action, as # described by the Contrast::Agent::Assess::Policy::TriggerNode class. # Each such method will call to this module just after invocation in # order to determine if the call was done safely. In those cases where # it was not, a Finding report is issued to the Service module TriggerMethod include Contrast::Components::Interface access_component :logging, :analysis # The level of TeamServer compliance our traces meet CURRENT_FINDING_VERSION = 2 class << self # This is called from within our woven proc. It will be called as if it # were inline in the Rack application. # # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] # the node that applies to the method being called # @param object [Object] the Object on which the method was invoked # @param ret [Object] the Return of the invoked method # @param args [Array] the Arguments with which the method # was invoked def apply_trigger_rule trigger_node, object, ret, args return if trigger_node.nil? current_context = Contrast::Agent::REQUEST_TRACKER.current return unless current_context&.analyze_request? && ASSESS.enabled? if trigger_node.sources&.any? trigger_node.sources.each do |marker| source = determine_source(marker, object, ret, args) apply_trigger(current_context, trigger_node, source, object, ret, 1, *args) end else apply_trigger(current_context, trigger_node, nil, object, ret, 1, *args) end end def apply_eval_trigger context, trigger_node, source, object, ret, invoked, *args apply_trigger(context, trigger_node, source, object, ret, invoked, *args) end # This converts the source of the finding, and the events leading # up to it into a Finding # # @param context [Contrast::Utils::ThreadTracker] the current request # context # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] # the node to direct applying this trigger event # @param source [Object] the source of the Trigger Event # @param object [Object] the Object on which the method was invoked # @param ret [Object] the Return of the invoked method # @param invoked [Integer] the depth of this invocation from # application code; often a lie. # @param args [Array] the Arguments with which the method # was invoked # @return [Contrast::Api::Dtm::Finding, nil] the # Contrast::Api::Dtm::Finding to send to TeamServer or nil if # conditions were not met def build_finding context, trigger_node, source, object, ret, invoked, *args return unless Contrast::Agent::Assess::Policy::TriggerValidation.valid?(trigger_node, object, ret, args) request = context.request env = request.env return if defined?(ActionController::Live) && env && env['action_controller.instance'].cs__class.included_modules.include?(ActionController::Live) finding = Contrast::Api::Dtm::Finding.new finding.rule_id = Contrast::Utils::StringUtils.protobuf_safe_string(trigger_node.rule_id) finding.session_id = Contrast::Agent::FeatureState.instance.current_session_id finding.version = CURRENT_FINDING_VERSION build_from_source(finding, source) trigger_event = Contrast::Agent::Assess::ContrastEvent.new(trigger_node, source, object, ret, args, invoked + 1).to_dtm_event finding.events << trigger_event build_hash(finding, source) build_tags(context) finding.routes << context.route if context.route context.activity.findings << finding logger.debug(nil, "Trigger #{ trigger_node.id } detected: #{ source.__id__ } triggered #{ trigger_node.rule_id }") rescue StandardError => e logger.error(e, "Unable to build a finding for #{ trigger_node.id }") end private def settings Contrast::Agent::FeatureState.instance end # This is our method that actually checks the taint on the object # our trigger_node targets. # # @param context [Contrast::Utils::ThreadTracker] the current request # context # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] # the node to direct applying this trigger event # @param source [Object] the source of the Trigger Event # @param object [Object] the Object on which the method was invoked # @param ret [Object] the Return of the invoked method # @param invoked [Integer] the depth of this invocation from # application code; often a lie. # @param args [Array] the Arguments with which the method # was invoked def apply_trigger context, trigger_node, source, object, ret, invoked, *args return unless context && trigger_node return if trigger_node.rule_disabled? return if trigger_node.dataflow? && source.nil? invoked += 1 if trigger_node.regexp_rule? apply_regexp_rule(context, trigger_node, source, object, ret, invoked, *args) elsif trigger_node.custom_trigger? trigger_node.apply_custom_trigger(context, trigger_node, source, object, ret, invoked, *args) elsif trigger_node.dataflow? apply_dataflow_rule(context, trigger_node, source, object, ret, invoked, *args) else # trigger rule - just calling the method is dangerous build_finding(context, trigger_node, source, object, ret, invoked, *args) end rescue StandardError => e logger.warn(e, 'Unable to apply trigger.') end # Given the marker from the trigger_node (the pointer indicating # the entity from which the taint originated), return the entity on # which this trigger needs to operate. # # In an effort to speed up this lookup, we've changed the marker # for parameters to be implicit - if it is not a return or an # object, it must be a parameter, which we can reference either by # index or by name. # # @param marker [String] the source marker that indicates which # Object # @param object [Object] the Object on which the method was invoked # @param ret [Object] the Return of the invoked method # @param args [Array] the Arguments with which the method # was invoked # @return [Object] the literal object that this Trigger Event # verifies def determine_source marker, object, ret, args case marker when Contrast::Utils::ObjectShare::RETURN_KEY ret when Contrast::Utils::ObjectShare::OBJECT_KEY object else # 'P' if marker.is_a?(Integer) args[marker] else arg = nil args.each do |search| next unless search.is_a?(Hash) arg = search[marker] break if arg end arg end end end # This is our method that actually checks the taint on the object # our trigger_node targets for our Regexp based rules. # # @param context [Contrast::Utils::ThreadTracker] the current request # context # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] # the node to direct applying this trigger event # @param source [Object] the source of the Trigger Event # @param object [Object] the Object on which the method was invoked # @param ret [Object] the Return of the invoked method # @param invoked [Integer] the depth of this invocation from # application code; often a lie. # @param args [Array] the Arguments with which the method # was invoked def apply_regexp_rule context, trigger_node, source, object, ret, invoked, *args return unless source.is_a?(String) return if trigger_node.good_value && source.match?(trigger_node.good_value) return if trigger_node.bad_value && source !~ trigger_node.bad_value invoked += 1 build_finding(context, trigger_node, source, object, ret, invoked, *args) end # This is our method that actually checks the taint on the object # our trigger_node targets for our Dataflow based rules. # # @param context [Contrast::Utils::ThreadTracker] the current request # context # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] # the node to direct applying this trigger event # @param source [Object] the source of the Trigger Event # @param object [Object] the Object on which the method was invoked # @param ret [Object] the Return of the invoked method # @param invoked [Integer] the depth of this invocation from # application code; often a lie. # @param args [Array] the Arguments with which the method # was invoked def apply_dataflow_rule context, trigger_node, source, object, ret, invoked, *args return unless source invoked += 1 if Contrast::Utils::DuckUtils.quacks_to?(source, :cs__properties) return unless source.cs__tracked? return unless trigger_node.violated?(source) build_finding(context, trigger_node, source, object, ret, invoked, *args) elsif Contrast::Utils::DuckUtils.iterable_hash?(source) invoked += 2 # the each & the block source.each_pair do |key, value| apply_dataflow_rule(context, trigger_node, key, object, ret, invoked, *args) apply_dataflow_rule(context, trigger_node, value, object, ret, invoked, *args) end elsif Contrast::Utils::DuckUtils.iterable_enumerable?(source) invoked += 2 # the each & the block source.each do |value| apply_dataflow_rule(context, trigger_node, value, object, ret, invoked, *args) end else logger.warn( nil, "Target is a #{ source.cs__class.name } -- not sure how to inspect: #{ trigger_node.inspect }") logger.debug(nil, source.to_s[0..99]) end end def build_from_source finding, source return unless source return unless Contrast::Utils::DuckUtils.quacks_to?( source, :cs__properties) return unless source.cs__properties # events could technically be nil, but we would have failed # the rule check before getting here. not worth the nil check source.cs__properties.events.each do |event| finding.events << event.to_dtm_event end # Google::Protobuf::Map doesn't support merge!, so we have to do this # long form source_props = source.cs__properties.properties return unless source_props source_props.each_pair do |key, value| key = Contrast::Utils::StringUtils.force_utf8(key) finding.properties[key] = Contrast::Utils::StringUtils.force_utf8(value) end end def build_hash finding, source hash_code = Contrast::Utils::HashDigest.generate_event_hash(finding, source) finding.hash_code = Contrast::Utils::StringUtils.force_utf8(hash_code) finding.preflight = Contrast::Utils::PreflightUtil.create_preflight(finding) end def build_tags context return unless ASSESS.tags context.activity.finding_tags = Contrast::Utils::StringUtils.force_utf8(ASSESS.tags) end end end end end end end