# 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/agent/protect/rule/base_service' cs__scoped_require 'contrast/components/interface' cs__scoped_require 'contrast/utils/stack_trace_utils' module Contrast module Agent module Protect module Rule # This class handles our implementation of the Path Traversal # Protect rule. class PathTraversal < Contrast::Agent::Protect::Rule::BaseService include Contrast::Components::Interface access_component :agent, :analysis NAME = 'path-traversal' SYSTEM_PATHS = %w[ /proc/self etc/passwd etc/shadow etc/hosts etc/groups etc/gshadow ntuser.dat /Windows/win.ini /windows/system32/ /windows/repair/ ].cs__freeze def name NAME end def infilter context, method, path return unless infilter?(context) result = find_attacker(context, path) return unless result append_to_activity(context, result) return unless blocked? raise Contrast::SecurityException.new( self, "Path Traversal rule triggered. Call to File.#{ method } blocked.") end protected def find_attacker context, path attack_result = nil attack_result = super(context, path) if infilter?(context) attack_result = check_rep_features(context, path, attack_result) attack_result end # Build a subclass of the RaspRuleSample using the query string and the # evaluation def build_sample context, input_analysis_result, path, **_kwargs sample = build_base_sample(context, input_analysis_result) sample.path_traversal = Contrast::Api::Dtm::PathTraversalDetails.new path ||= input_analysis_result.value sample.path_traversal.path = Contrast::Utils::StringUtils.protobuf_safe_string(path) sample end private # Build a subclass of the RaspRuleSample if the sample matches def build_rep_sample context, path sample = build_base_sample(context, nil) sample.path_traversal_semantic = Contrast::Api::Dtm::PathTraversalSemanticAnalysisDetails.new path = Contrast::Utils::StringUtils.protobuf_safe_string(path) sample.path_traversal_semantic.path = path if custom_code_access_sysfile_enabled? && custom_code_accessing_system_file?(path) sample.path_traversal_semantic.findings << :CUSTOM_CODE_ACCESSING_SYSTEM_FILES return sample end if common_file_exploits_enabled? && contains_known_attack_signatures?(path) sample.path_traversal_semantic.findings << :COMMON_FILE_EXPLOITS return sample end nil end def check_rep_features context, path, attack_result rep_sample = build_rep_sample(context, path) if rep_sample attack_result = build_attack_result(context) if attack_result.nil? build_attack_with_match(context, nil, attack_result, path) end attack_result end def custom_code_access_sysfile_enabled? PROTECT.report_custom_code_sysfile_access? end def custom_code_accessing_system_file? input system_file?(input) && Contrast::Utils::StackTraceUtils.custom_code_context? end def system_file? path return false unless path SYSTEM_PATHS.any? { |sys_path| sys_path.include?(path) } end def common_file_exploits_enabled? false end # TODO: RUBY-318 # KNOWN_SECURITY_BYPASS_MARKERS = ['::$DATA', '::$Index', '', '\x00'].cs__freeze def contains_known_attack_signatures? input utf8 = Contrast::Utils::StringUtils.force_utf8(input) _ = CGI.unescape(utf8) # TODO: RUBY-318 implement REP for known attack signatures # try: # realpath = os.path.realpath(unescaped).lower().rstrip('/') # except ValueError as e: # return 'embedded null byte' == str(e) # except TypeError as e: # return 'NUL' in str(e) or 'null byte' in str(e) or (PY34 and 'embedded NUL character' == str(e)) # except Exception as e: # return 'null byte' in str(e).lower() # return return any([bypass_markers.lower().rstrip('/') in realpath for bypass_markers in PathTraversalREPMixin.KNOWN_SECURITY_BYPASS_MARKERS]) false end end end end end end