# 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/components/interface' cs__scoped_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::Interface access_component :logging 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::Utils::DuckUtils.quacks_to?(source, :cs__tracked?) && source.cs__tracked? args = preshift.args[1] incoming_tracked = args && determine_tracked(args) return ret unless self_tracked || incoming_tracked if block block_sub(self_tracked, source, ret) elsif args.is_a?(String) string_sub(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(preshift, ret) end string_build_event(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 if args.is_a?(String) Contrast::Utils::DuckUtils.quacks_to?(args, :cs__tracked?) && args.cs__tracked? # if it's a hash, ask if it has a tracked string elsif args.is_a?(Hash) args.values.any? { |value| value.is_a?(String) && Contrast::Utils::DuckUtils.quacks_to?(value, :cs__tracked?) && value.cs__tracked? } # this should never happen else false end end def string_sub self_tracked, preshift, ret, incoming, incoming_tracked, global pattern = preshift.args[0] 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) source.cs__splat_tags(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 ret.cs__copy_from(source) # 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 ret.cs__properties.delete_tags_at_ranges(ranges) ret.cs__properties.shift_tags(ranges) return unless incoming_tracked tags = incoming.cs__properties.tag_keys ranges.each do |range| tags.each do |tag| ret.cs__properties.add_tag(tag, range) end end end def block_sub self_tracked, source, ret source.cs__splat_tags(ret) if self_tracked end def hash_sub self_tracked, source, ret source.cs__splat_tags(ret) if self_tracked end def pattern_gsub preshift, ret return unless Contrast::Utils::DuckUtils.trackable?(ret) source = preshift.object source.cs__properties.tag_keys.each do |key| ret.cs__properties.add_tag(key, 0...1) end end def string_build_event patcher, preshift, ret return unless Contrast::Utils::DuckUtils.quacks_to?(ret, :cs__tracked?) && ret.cs__tracked? args = preshift.args if args.length > 1 arg = args[1] if Contrast::Utils::DuckUtils.quacks_to?(arg, :cs__properties) arg.cs__properties.events.each do |event| ret.cs__properties.events << event end end end ret.cs__properties.build_event( patcher, ret, preshift.object, ret, args, 2) end end end end end end end end