# Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/components/logger' require 'contrast/utils/duck_utils' module Contrast module Agent module Assess module Policy module Propagator # This class is specifically for String#(g)sub propagation # # Disclaimer: there may be a better way, but we're # in a 'get it work' state. hopefully, we'll be in # a 'get it right' state soon. class Substitution include Contrast::Components::Logger::InstanceMethods extend Contrast::Components::Logger::InstanceMethods CAPTURE_GROUP_REGEXP = /\\[[:digit:]]/.cs__freeze CAPTURE_NAME_REGEXP = /\\k<[[:alpha:]]/.cs__freeze class << self # gsub is hard. there are four versions of this method # 1) pattern, replacement (my fav) # 2) pattern, hash (not bad) # 3) pattern, block (are you kidding me?) # 4) pattern (plz no) # # In addition, it requires things from $~ & $1-9, which # are method scoped. Rather than fight that, we'll # call gsub from C land using a CUSTOM patch. def gsub_tagger patcher, preshift, ret, block substitution_tagger(patcher, preshift, ret, !block.nil?) end def sub_tagger patcher, preshift, ret, block substitution_tagger(patcher, preshift, ret, !block.nil?, false) end private def substitution_tagger patcher, preshift, ret, block, global = true return ret unless ret begin source = preshift.object self_tracked = Contrast::Agent::Assess::Tracker.tracked?(source) args = preshift.args[1] incoming_tracked = args && determine_tracked(args) return ret unless self_tracked || incoming_tracked parent_events = [] if block block_sub(self_tracked, source, ret) elsif args.is_a?(String) string_sub(parent_events, self_tracked, preshift, ret, args, incoming_tracked, global) elsif args.is_a?(Hash) hash_sub(self_tracked, source, ret) else # Enumerator, only for gsub pattern_gsub(parent_events, preshift, ret) end if self_tracked source_properties = Contrast::Agent::Assess::Tracker.properties(source) parent_event = source_properties&.event parent_events.prepend(parent_event) if parent_event end string_build_event(parent_events, patcher, preshift, ret) rescue StandardError => e logger.error('Unable to apply gsub propagator', e) end ret end def determine_tracked args # if there's no arg, it can't be tracked return false unless args # if it's a string, just ask if it's tracked case args when String Contrast::Agent::Assess::Tracker.tracked?(args) # if it's a hash, ask if it has a tracked string when Hash args.values.any? { |value| value.is_a?(String) && Contrast::Agent::Assess::Tracker.tracked?(value) } # this should never happen else false end end def string_sub parent_events, self_tracked, preshift, ret, incoming, incoming_tracked, global return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret)) incoming_properties = Contrast::Agent::Assess::Tracker.properties(incoming) parent_events << incoming_properties&.event if incoming_properties&.event source = preshift.object # We can't efficiently find the places that things were # copied from regexp / captures. Trading accuracy for # performance if incoming.match?(CAPTURE_GROUP_REGEXP) || incoming.match?(CAPTURE_NAME_REGEXP) properties.splat_from(source, ret) if self_tracked return end # if it's just a straight insert, that we can do # Copy the tags from us to the return ranges = find_string_sub_insert(properties, preshift, incoming, ret, global) properties.delete_tags_at_ranges(ranges) properties.shift_tags(ranges) return unless incoming_tracked return unless incoming_properties tags = incoming_properties.tag_keys ranges.each do |range| tags.each do |tag| properties.add_tag(tag, range) end end end # Find the points at which the new String was placed into the original # # @param properties [Contrast::Agent::Assess::Properties] the Properties of the ret # @param preshift [Contrast::Agent::Assess::PreShift] the capture of the state of the code just prior to # the invocation of the patched method # @param incoming [String] the new String going into the substitution # @param ret [String] the result of the substitution # @param global [Boolean] if this was a global or single substitution # @return [Array] the Ranges where substitution occurred def find_string_sub_insert properties, preshift, incoming, ret, global pattern = preshift.args[0] source = preshift.object properties.copy_from(source, ret) # Figure out where inserts occurred last_idx = 0 ranges = [] # For each insert, move the tag ranges while last_idx idx = source.index(pattern, last_idx) break unless idx last_idx = idx ? idx + 1 : nil start_index = idx end_index = idx + incoming.length ranges << (start_index...end_index) break unless global end ranges end def block_sub self_tracked, source, ret return unless self_tracked properties = Contrast::Agent::Assess::Tracker.properties!(ret) properties&.splat_from(source, ret) end def hash_sub self_tracked, source, ret return unless self_tracked properties = Contrast::Agent::Assess::Tracker.properties!(ret) properties&.splat_from(source, ret) end def pattern_gsub parent_events, preshift, ret source = preshift.object return unless (source_properties = Contrast::Agent::Assess::Tracker.properties(source)) return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret)) source_properties.tag_keys.each do |key| properties.add_tag(key, 0...1) end parent_event = source_properties.event parent_events << parent_event if parent_event end def string_build_event parent_events, patcher, preshift, ret return unless Contrast::Agent::Assess::Tracker.tracked?(ret) properties = Contrast::Agent::Assess::Tracker.properties(ret) args = preshift.args properties.build_event(patcher, ret, preshift.object, ret, args, 2) properties.event.instance_variable_set(:@_parent_events, parent_events) end end end end end end end end