# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/utils/object_share' require 'contrast/agent/protect/input_analyzer/input_analyzer' require 'contrast/agent/protect/rule/input_classification/extendable' require 'contrast/agent/protect/rule/input_classification/encoding' require 'contrast/components/logger' module Contrast module Agent module Protect module Rule module InputClassification # This module will include all the similar information for all input classifications # between different rules module Base UNKNOWN_KEY = 'unknown' include Contrast::Components::Logger::InstanceMethods include Contrast::Agent::Protect::Rule::InputClassification::Extendable include Contrast::Agent::Protect::Rule::InputClassification::Encoding KEYS_NEEDED = [ COOKIE_VALUE, PARAMETER_VALUE, HEADER, JSON_VALUE, MULTIPART_VALUE, XML_VALUE, DWR_VALUE ].cs__freeze BASE64_INPUT_TYPES = [BODY, HEADER, COOKIE_VALUE, PARAMETER_VALUE, MULTIPART_VALUE, XML_VALUE].cs__freeze class << self include Contrast::Components::Logger::InstanceMethods include Contrast::Agent::Reporting::InputType # Finds key value and type based on input type and value. # @param request [Contrast::Agent::Request] the current request context. # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input. # @param value [String, Array<String>] the value of the input. # @return [Array<(String, Contrast::Agent::Reporting::InputType)>] key and key type. def find_key request, input_type, value # TODO: RUBY-99999 Add handling for multipart, json and if any missing types. case input_type when COOKIE_VALUE [request.cookies.key(value), Contrast::Agent::Reporting::InputType::COOKIE_NAME] when PARAMETER_VALUE, URL_PARAMETER [request.parameters.key(value), Contrast::Agent::Reporting::InputType::PARAMETER_NAME] when HEADER [request.headers.key(value), Contrast::Agent::Reporting::InputType::HEADER] when UNKNOWN [UNKNOWN_KEY, Contrast::Agent::Reporting::InputType::UNKNOWN] else [nil, nil] end rescue StandardError => e logger.warn('[InputAnalyzer] Could not find proper key for input traced value', message: e) [nil, nil] end # Some input types are not yet supported from the AgentLib. # This will convert the type to the closet possible if viable, # so that the input tracing could be done. # # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input. # @return [Integer<Contrast::AgentLib::Interface::INPUT_SET>] def convert_input_type input_type case input_type when URI, URL_PARAMETER Contrast::AGENT_LIB.input_set[:URI_PATH] when BODY, DWR_VALUE, SOCKET, UNDEFINED_TYPE, UNKNOWN, REQUEST, QUERYSTRING Contrast::AGENT_LIB.input_set[:PARAMETER_VALUE] when HEADER Contrast::AGENT_LIB.input_set[:HEADER_VALUE] when MULTIPART_VALUE, MULTIPART_FIELD_NAME Contrast::AGENT_LIB.input_set[:MULTIPART_NAME] when JSON_ARRAYED_VALUE Contrast::AGENT_LIB.input_set[:JSON_KEY] when PARAMETER_NAME Contrast::AGENT_LIB.input_set[:PARAMETER_KEY] else Contrast::AGENT_LIB.input_set[input_type] end rescue StandardError => e logger.debug('[InputAnalyzer] Protect Input classification could not determine input type, falling back to default', error: e) Contrast::AGENT_LIB.input_set[:PARAMETER_VALUE] end end # Input Classification stage is done to determine if an user input is # DEFINITEATTACK or to be ignored. # # @param rule_id [String] Name of the protect rule. # @param input_type [Symbol, Contrast::Agent::Reporting::InputType] The type of the user input. # @param value [String, Array<String>] the value of the input. # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Holds all the results from the # agent analysis from the current # Request. # @return ia [Contrast::Agent::Reporting::InputAnalysis, nil] with updated results. def classify rule_id, input_type, value, input_analysis return unless (rule = Contrast::PROTECT.rule(rule_id)) return unless rule.applicable_user_inputs.include?(input_type) return unless input_analysis.request Array(value).each do |val| Array(val).each do |v| next unless v result = create_new_input_result(input_analysis.request, rule.rule_name, input_type, v) append_result(input_analysis, result) end end input_analysis rescue StandardError => e logger.debug("An Error was recorded in the input classification of the #{ rule_id }", error: e) nil end # This methods checks if input is value that matches a key in the input. # # @param request [Contrast::Agent::Request] the current request context. # @param ia_result [Contrast::Agent::Reporting::InputAnalysisResult] result to be updated. # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input. # @param value [String, Array<String>] the value of the input. # # @return result [Array<String, Symbol>] updated with key result. def add_needed_key request, ia_result, input_type, value ia_result.key, ia_result.key_type = Contrast::Agent::Protect::Rule::InputClassification::Base. find_key(request, input_type, value) end private # Appends result to the InputAnalysis. # # @param ia_analysis [Contrast::Agent::Reporting::InputAnalysis] the current input analysis. # @param result [Contrast::Agent::Reporting::InputAnalysisResult] result to be appended. # @return [Contrast::Agent::Reporting::InputAnalysis] the input analysis with the appended result. def append_result ia_analysis, result ia_analysis.results << result if result ia_analysis end # Do not override this method, it will hold base operations, instead overwrite methods called inside # of this method. # This methods checks if input is tagged WORTHWATCHING or IGNORE matches value with it's # key if needed and Creates new instance of InputAnalysisResult. # # @param request [Contrast::Agent::Request] the current request context. # @param rule_id [String] The name of the Protect Rule. # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input. # @param value [String, Array<String>] the value of the input. # # @return res [Contrast::Agent::Reporting::InputAnalysisResult, nil] def create_new_input_result request, rule_id, input_type, value return unless Contrast::AGENT_LIB # Cache retrieve cached = Contrast::Agent::Protect::InputAnalyzer.lru_cache.lookout(rule_id, value, input_type, request) return cached.result if cached.cs__is_a?(Contrast::Agent::Protect::InputClassification::CachedResult) # Input evaluation input_eval = build_input_eval(rule_id, input_type, base64_decode_input(value, input_type)) ia_result = build_ia_result(rule_id, input_type, value, request, input_eval) return unless ia_result add_needed_key(request, ia_result, input_type, value) if KEYS_NEEDED.include?(input_type) # Input evaluation end # Cache save. Cache must be saved after the input evaluation is completed. Contrast::Agent::Protect::InputAnalyzer.lru_cache.save(rule_id, ia_result, request) ia_result end # Decodes the value for the given input type. # # This applies to Values sources only: # BODY, COOKIE_VALUE, HEADER, PARAMETER_VALUE, MULTIPART_VALUE, XML_VALUE # # @param value [String] # @param input_type [Symbol] # @return input [String] def base64_decode_input value, input_type return value unless Contrast::PROTECT.normalize_base64? return value unless BASE64_INPUT_TYPES.include?(input_type) cs__decode64(value, input_type) end end end end end end end