# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/protect/rule/base_service' require 'contrast/utils/stack_trace_utils' require 'contrast/agent/reporting/details/path_traversal_details' require 'contrast/agent/reporting/details/path_traversal_semantic_analysis_details' 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 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 class << self # @param attack_sample [Contrast::Api::Dtm::RaspRuleSample] # @return [Hash] the details for this specific rule def extract_details attack_sample { path: attack_sample.path_traversal.path } end end def rule_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? cef_logging(result, :successful_attack) 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) check_rep_features(context, path, 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.details = Contrast::Agent::Reporting::Details::PathTraversalDetails.new path ||= input_analysis_result.value sample.details.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.details = Contrast::Agent::Reporting::Details::PathTraversalSemanticAnalysisDetails.new path = Contrast::Utils::StringUtils.protobuf_safe_string(path) sample.details.path = path if custom_code_access_sysfile_enabled? && custom_code_accessing_system_file?(path) sample.details.findings << :CUSTOM_CODE_ACCESSING_SYSTEM_FILES return sample end if common_file_exploits_enabled? && contains_known_attack_signatures?(path) sample.details.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? ::Contrast::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