# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/protect/rule/path_traversal' require 'contrast/agent/protect/policy/rule_applicator' require 'contrast/utils/object_share' module Contrast module Agent module Protect module Policy # 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 extend Contrast::Agent::Protect::Policy::RuleApplicator class << self def invoke method, _exception, properties, object, args return unless args&.any? path = args[0] return unless path.is_a?(String) return if skip_analysis? action = properties['action'] write_marker = write?(action, *args) possible_write = write_marker && possible_write(write_marker) path_traversal_rule(path, possible_write, object, 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) path_traversal_rule(dst, true, object, method) end protected def name Contrast::Agent::Protect::Rule::PathTraversal::NAME end private def possible_write input input.cs__respond_to?(:to_s) && input.to_s.include?(Contrast::Utils::ObjectShare::WRITE_FLAG) 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 && possible_write(write_marker) end def path_traversal_rule path, possible_write, object, method return unless applies_to?(path, possible_write) rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, method, path) rescue Contrast::SecurityException => e raise e rescue StandardError => e logger.error('Error applying path traversal', e, module: object.cs__class.cs__name, method: method) end CS__SAFER_REL_PATHS = %w[public app log tmp].cs__freeze def safer_abs_paths @_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 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) 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 end