# 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/components/interface' module Contrast module CoreExtensions module Protect # This Module is how we apply the Path Traversal rule. It is called from # our patches of the targeted methods in which File or IO access occur. # It is responsible for deciding if the infilter methods of the rule # should be invoked. module AppliesPathTraversalRule include Contrast::Components::Interface access_component :logging, :analysis class << self def cs__possible_write input input.cs__respond_to?(:to_s) && input.to_s.include?(Contrast::Utils::ObjectShare::WRITE_FLAG) end def rule PROTECT.rule Contrast::Agent::Protect::Rule::PathTraversal::NAME end def cs__skip_analysis? context = Contrast::Agent::REQUEST_TRACKER.current return true unless context&.app_loaded? return true unless rule&.enabled? end def cs__patched_apply_path_traversal_rule method, _exception, properties, _object, args return unless args&.any? path = args[0] return unless path.is_a?(String) return if cs__skip_analysis? action = properties['action'] write_marker = write?(action, *args) possible_write = write_marker && cs__possible_write(write_marker) cs__patched_path_traversal_rule(path, possible_write, method) # If the action was copy, we need to handle the write half of it. # We handled read in line above. return unless action == COPY return unless args.length > 1 dst = args[1] return unless dst.is_a?(String) cs__patched_path_traversal_rule(dst, true, method) end READ = 'read' WRITE = 'write' COPY = 'copy' def write? action, *args return false if action == READ return false if action == COPY return true if action == WRITE write_marker = args.length > 1 ? args[1] : nil write_marker && cs__possible_write(write_marker) end def cs__patched_path_traversal_rule path, possible_write, method return unless cs__patched_applies_to?(path, possible_write) logger.debug(nil, "checking path traversal: write=true path=#{ path }") rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, method, path) rescue Contrast::SecurityException => e raise e rescue StandardError => e logger.error(e, 'path traversal') end CS__SAFER_REL_PATHS = %w[public app log tmp].cs__freeze def cs__patched_safer_abs_paths @_cs__patched_safer_abs_paths ||= begin pwd = ENV['PWD'] if pwd tmp = CS__SAFER_REL_PATHS.map { |r| "#{ pwd }/#{ r }" } gems = ENV['GEM_PATH'] tmp += gems.split(Contrast::Utils::ObjectShare::COLON) if gems tmp else [] end end end def cs__patched_applies_to? path, possible_write = false # any possible write is a potential risk return true if possible_write # any path that moves 'up' is a potential risk return true if path.index(Contrast::Utils::ObjectShare::PARENT_PATH) path = path.downcase if path.start_with?(Contrast::Utils::ObjectShare::SLASH) cs__patched_safer_abs_paths.each do |prefix| return false if path.start_with?(prefix) end else CS__SAFER_REL_PATHS.each do |prefix| return false if path.start_with?(prefix) end end true end end end end end end