# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true # This class is responsible for the origination of traces. A Source is any method # that returns an untrusted value. In general, these sources are request methods # as the request object is user controlled. # # Going forward, we may add in Dynamic sources (determined at runtime) based on # database configs or other variables (used for Stored XSS or other persisted # vulnerability detection rules) cs__scoped_require 'set' cs__scoped_require 'contrast/utils/object_share' cs__scoped_require 'contrast/utils/sha256_builder' cs__scoped_require 'contrast/agent/assess/adjusted_span' cs__scoped_require 'contrast/components/interface' module Contrast module Agent module Assess module Policy # This class controls the actions we take on Sources, as determined by # our Assess policy. It indicates what actions we should take in order # to mark data as User Input and treat it as untrusted, starting the # dataflows used in Assess vulnerability detection. module SourceMethod include Contrast::Components::Interface access_component :logging, :analysis def self.determine_target source_node, object, ret, args source_target = source_node.targets[0] case source_target when Contrast::Utils::ObjectShare::RETURN_KEY ret when Contrast::Utils::ObjectShare::OBJECT_KEY object else if source_target.is_a?(Integer) args[source_target] # If this isn't an index param, it's a named one. R.I.P. else arg = nil args.each do |search| next unless search.is_a?(Hash) arg = search[source_target] break if arg end arg end end end # This is called from within our woven proc. It will be called as if it # were inline in the Rack application. def self.source_patchers method_policy, object, ret, args return if method_policy.source_node.nil? current_context = Contrast::Agent::REQUEST_TRACKER.current return unless current_context&.analyze_request? && ASSESS.enabled? replaced_return = nil source_node = method_policy.source_node target = determine_target(source_node, object, ret, args) # We don't propagate to frozen things that haven't been tracked # before. By definition, something that is a source has not # previously been tracked; therefore, we can break out early. if target.cs__frozen? # That being said, we don't have enough context to know if we # can make this assumption and still function, so we'll allow for # source tracking of frozen things by a common config setting. # # Rails' StrongParameters make a case for this to be default # behavior return replaced_return unless ASSESS.track_frozen_sources? # If we're tracking the frozen target, we need to unfreeze # (dup) it to track and then freeze that result. For # simplicities sake, we ONLY do this if the return is the # target (I don't want to have to deal with unfreezing self) return replaced_return unless source_node.targets[0] == Contrast::Utils::ObjectShare::RETURN_KEY restore_frozen_state = true ret = Contrast::Utils::FreezeUtil.unfreeze_dup(ret) target = ret end SourceMethod.cs__apply_source(current_context, source_node, target, object, ret, *args) ret.cs__freeze if restore_frozen_state ret end # This is our method that actually taints the object our source_node # targets. def self.cs__apply_source context, source_node, target, object, ret, *args return unless context source_node_source = source_node.sources[0] source_name = case source_node_source when nil nil when Contrast::Utils::ObjectShare::RETURN_KEY ret when Contrast::Utils::ObjectShare::OBJECT_KEY self else args[source_node_source] end _cs__apply_source context, source_node, target, object, ret, source_node.type, source_name, 0, *args end # I lied above. We had to figure out what the target of the source was. # Now that we know, we'll actually tag it. def self._cs__apply_source context, source_node, target, object, ret, source_type, source_name = nil, invoked = 0, *args return unless context && source_node && target # We know we only work on certain things. # Skip if this isn't one of them if Contrast::Utils::DuckUtils.quacks_to?(target, :cs__properties) # don't apply second source -- probably needs tuning later if we # use more than 'UNTRUSTED' in our sources return if target.cs__tracked? || target.cs__frozen? # otherwise for each tag this source_node applies, create a tag range # on the target object # I realize this looping is counter-intuitive from the above # message, that's why we're revisiting. source_node.tags.each do |tag| length = Contrast::Utils::StringUtils.ret_length(target) target.cs__properties.add_tag(tag, Contrast::Agent::Assess::AdjustedSpan.new(0, length)) target.cs__properties.add_properties(source_node.properties) logger.debug(nil, "Source #{ source_node.id } detected: #{ target.__id__ } tagged with #{ tag }") end # make a representation of this method that TeamServer can render target.cs__properties.build_event(source_node, target, object, ret, args, invoked, source_type, source_name) # While we don't taint hashes themselves, we may taint the things # they hold. Let's pass their keys and values back to ourselves and # try again elsif Contrast::Utils::DuckUtils.quacks_like_tracked_hash?(target) source_key_type = invoked.zero? ? key_type(source_type) : source_type invoked += 1 to_replace = [] target.each_pair do |key, value| # We only do this for Strings b/c of the way Hash lookup works. # To replace another object would break hash lookup and, # therefore, the application if ASSESS.track_frozen_sources? && key.is_a?(String) && Contrast::Utils::DuckUtils.quacks_to?(target, :delete) key = Contrast::Utils::FreezeUtil.unfreeze_dup(key) to_replace << key end _cs__apply_source(context, source_node, key, object, ret, source_key_type, key, invoked, *args) _cs__apply_source(context, source_node, value, object, ret, source_type, key, invoked, *args) end # Hash is designed to keep one instance of the string key in it. # We need to remove the existing one and replace it with our new # tracked one. to_replace.each do |key| key.cs__freeze value = target[key] target.delete(key) target[key] = value end # While we don't taint arrays themselves, we may taint the things # they hold. Let's pass their keys and values back to ourselves and # try again elsif Contrast::Utils::DuckUtils.quacks_like_tracked_enumerable?(target) invoked += 1 target.each { |value| _cs__apply_source(context, source_node, value, object, ret, source_type, source_name, invoked, *args) } end rescue StandardError => e logger.warn(e, "Unable to apply source for source_node #{ source_node.id }") end # Silly helper method so that TeamServer can properly mark up # the source of this trace, if this source ends up in a trigger PARAMETER_TYPE = 'PARAMETER' PARAMETER_KEY_TYPE = 'PARAMETER_KEY' HEADER_TYPE = 'HEADER' HEADER_KEY_TYPE = 'HEADER_KEY' COOKIE_TYPE = 'COOKIE' COOKIE_KEY_TYPE = 'COOKIE_KEY' def self.key_type source_type case source_type when PARAMETER_TYPE PARAMETER_KEY_TYPE when HEADER_TYPE HEADER_KEY_TYPE when COOKIE_TYPE COOKIE_KEY_TYPE else source_type end end end end end end end