# Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/extension/module' require 'contrast/utils/object_share' module Contrast module Utils # Utility methods for exploring the complete space of Objects class ClassUtil class << self # some classes have had things prepended to them, like Marshal in Rails # 5 and higher. Their ActiveSupport::MarshalWithAutoloading will break # our alias patching approach, as will any other prepend on something # that we touch. Prepend and Alias are inherently incompatible monkey # patching approaches. As such, we need to know if something has been # prepended to. # # @param mod [Module] the Module to check to see if it has had something # prepended # @param ancestors [Array] the array of ancestors for the mod # @return [Boolean] if the mod has been prepended or not def prepended? mod, ancestors = nil ancestors ||= mod.ancestors ancestors[0] != mod end # return true if the given method is overwritten by one of the ancestors # in the ancestor change that comes before the given module def prepended_method? mod, method_policy target_module = determine_target_class mod, method_policy.instance_method ancestors = target_module.ancestors return false unless prepended?(target_module, ancestors) ancestors.each do |ancestor| break if ancestor == target_module methods = ancestor.instance_methods(false) return true if methods.include?(method_policy.method_name) end false end # Return a String representing the object invoking this method in the # form expected by our dataflow events. # # @param object [Object, nil] the entity to convert to a String # @return [String] the human readable form of the String, as defined by # https://bitbucket.org/contrastsecurity/assess-specifications/src/master/vulnerability/capture-snapshot.md def to_contrast_string object # Only treat object like a string if it actually is a string # some subclasses of String override string methods we depend on if object.cs__class == String cached = to_cached_string(object) return cached if cached object.dup elsif object.nil? Contrast::Utils::ObjectShare::NIL_STRING elsif object.cs__is_a?(Symbol) ":#{ object }" elsif object.cs__is_a?(Module) || object.cs__is_a?(Class) "#{ object.cs__name }@#{ object.__id__ }" elsif object.cs__is_a?(Regexp) object.source elsif use_to_s?(object) object.to_s else "#{ object.cs__class.cs__name }@#{ object.__id__ }" end end # The method const_defined? can cause autoload, which is bad for us. # The method autoload? doesn't traverse namespaces. This method lets us # provide a constant, as a String, and parse it to determine if it has # been truly truly defined, meaning it existed before this method was # invoked, not as a result of it. # # This is required to handle a bug in Ruby prior to 2.7.0. When we drop # support for 2.6.X, we should remove this code. # https://bugs.ruby-lang.org/issues/10741 # @param name [String] the name of the constant to look up # @return [Boolean] def truly_defined? name return false unless name segments = name.split(Contrast::Utils::ObjectShare::DOUBLE_COLON) previous_module = Module segments.each do |segment| return false if previous_module.cs__autoload?(segment) return false unless previous_module.cs__const_defined?(segment) previous_module = previous_module.cs__const_get(segment) end true rescue NameError # account for nonsense / poorly formatted constants false end private # Some objects have nice to_s that we can use to make them human readable. If they do, we should leverage them. # We used to do this by default, but this opened us up to danger, so we're instead using an allow list approach. # # @param object [Object] something that may have a safe to_s method # @return [Boolean] if we should invoke to_s to represent the object def use_to_s? object return true if object.cs__is_a?(Numeric) return true if defined?(Arel::Nodes::SqlLiteral) && object.cs__is_a?(Arel::Nodes::SqlLiteral) false end def determine_target_class mod, is_instance return mod if mod.singleton_class? return mod.cs__singleton_class unless is_instance mod end # If the String matches a common String in our ObjectShare, return that # rather that for use as the representation of the String rather than # forcing a duplication of the String. # # @param string [String] some string of which we want a Contrast # representation. # @return [String,nil] the ObjectShare version of the String or nil def to_cached_string string return Contrast::Utils::ObjectShare::EMPTY_STRING if string.empty? return Contrast::Utils::ObjectShare::SLASH if string == Contrast::Utils::ObjectShare::SLASH return Contrast::Utils::ObjectShare::EQUALS if string == Contrast::Utils::ObjectShare::EQUALS nil end end end end end