# 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/agent/protect/rule/path_traversal/path_traversal_semantic_security_bypass' require 'contrast/agent/reporting/input_analysis/input_type' require 'contrast/agent/reporting/input_analysis/score_level' require 'contrast/utils/stack_trace_utils' require 'contrast/agent/reporting/details/path_traversal_details' require 'contrast/agent/reporting/details/path_traversal_semantic_analysis_details' require 'contrast/agent/protect/rule/path_traversal/path_traversal_input_classification' require 'contrast/utils/string_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::Agent::Reporting::InputType NAME = 'path-traversal' APPLICABLE_USER_INPUTS = [ BODY, COOKIE_NAME, COOKIE_VALUE, HEADER, PARAMETER_VALUE, PARAMETER_NAME, JSON_VALUE, MULTIPART_VALUE, MULTIPART_FIELD_NAME, XML_VALUE, DWR_VALUE, URI ].cs__freeze 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 rule_name NAME end # Array of sub_rules # # @return [Array] def sub_rules @_sub_rules ||= [Contrast::Agent::Protect::Rule::PathTraversalSemanticBypass.new].cs__freeze end def applicable_user_inputs APPLICABLE_USER_INPUTS end # Path Traversal input classification # # @return [module] def classification @_classification ||= Contrast::Agent::Protect::Rule::PathTraversalInputClassification.cs__freeze 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? result_rule_name = Contrast::Utils::StringUtils.transform_string(result.rule_id) cef_logging(result, :successful_attack, value: path) exception_messasge = "#{ result_rule_name } rule triggered. Call to File.#{ method } blocked." raise(Contrast::SecurityException.new(self, exception_messasge)) 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 # @param context [Contrast::Agent::RequestContext] def infilter? context return false unless enabled? return false unless context&.agent_input_analysis&.results&.any? do |result| # When a file is being accessed, the agent should see if any of its worth-watching inputs appear in # the file path. If so, the input is considered a confirmed attack and should be reported or blocked. # If the score level is ignore we don't need to report it. result.rule_id == rule_name && result.score_level != Contrast::Agent::Reporting::ScoreLevel::IGNORE end return false if protect_excluded_by_code? true 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