# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/reporting/input_analysis/input_type' require 'contrast/agent/reporting/input_analysis/score_level' require 'contrast/agent/reporting/input_analysis/input_analysis' require 'contrast/agent/protect/rule/bot_blocker' require 'contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification' require 'contrast/agent/protect/rule/cmdi/cmdi_input_classification' require 'contrast/agent/protect/rule/no_sqli' require 'contrast/agent/protect/rule/no_sqli/no_sqli_input_classification' require 'contrast/agent/protect/rule/sqli/sqli_input_classification' require 'contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification' require 'contrast/agent/protect/rule/unsafe_file_upload' require 'contrast/agent/protect/rule/path_traversal' require 'contrast/agent/protect/rule/path_traversal/path_traversal_input_classification' require 'contrast/agent/protect/rule/xss/reflected_xss_input_classification' require 'contrast/agent/protect/rule/xss' require 'contrast/agent/protect/rule/http_method_tampering/http_method_tampering_input_classification' require 'contrast/components/logger' require 'contrast/utils/object_share' require 'json' module Contrast module Agent module Protect # InputAnalyzer will extract input form current request context and will analyze it. # This will be used in for the SQLI and CMDI worth_watching_v2 implementations. module InputAnalyzer DISPOSITION_NAME = 'name' DISPOSITION_FILENAME = 'filename' DISPOSITION_KEYS = %w[Content-Disposition CONTENT_DISPOSITION].cs__freeze class << self include Contrast::Agent::Reporting::InputType include Contrast::Agent::Reporting::ScoreLevel include Contrast::Utils::ObjectShare include Contrast::Components::Logger::InstanceMethods PROTECT_RULES = { sqli: { rule_name: 'sql-injection', classification: Contrast::Agent::Protect::Rule::SqliInputClassification }, cmdi: { rule_name: 'cmd-injection', classification: Contrast::Agent::Protect::Rule::CmdiInputClassification }, # method_tampering: { # rule_name: 'method-tampering', # classification: Contrast::Agent::Protect::Rule::HttpMethodTamperingInputClassification # }, reflected_xss: { rule_name: Contrast::Agent::Protect::Rule::Xss::NAME, classification: Contrast::Agent::Protect::Rule::ReflectedXssInputClassification }, bot_blocker: { rule_name: Contrast::Agent::Protect::Rule::BotBlocker::NAME, classification: Contrast::Agent::Protect::Rule::BotBlockerInputClassification }, unsafe_file_upload: { rule_name: Contrast::Agent::Protect::Rule::UnsafeFileUpload::NAME, classification: Contrast::Agent::Protect::Rule::UnsafeFileUploadInputClassification }, path_traversal: { rule_name: Contrast::Agent::Protect::Rule::PathTraversal::NAME, classification: Contrast::Agent::Protect::Rule::PathTraversalInputClassification }, nosqli: { rule_name: Contrast::Agent::Protect::Rule::NoSqli::NAME, classification: Contrast::Agent::Protect::Rule::NoSqliInputClassification } }.cs__freeze # This method with analyze the user input from the context of the # current request and run each of the protect rules against all # found input types # # @param request [Contrast::Agent::Request] current request context. # @return input_analysis [Contrast::Agent::Reporting::InputAnalysis, nil] def analyse request return unless Contrast::PROTECT.enabled? return if request.nil? inputs = extract_input(request) return unless inputs input_analysis = Contrast::Agent::Reporting::InputAnalysis.new input_analysis.request = request # each rule against each input input_classification(inputs, input_analysis) input_analysis end private # classify input by rule implementation of worth_watching_v2 for the rules supporting it. # # @param inputs [String, Array] user input to be analysed. # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Here we will keep all the results # for each protect rule. # @return input_analysis [Contrast::Agent::Reporting::InputAnalysis, nil] def input_classification inputs, input_analysis # key = input type, value = user_input inputs.each do |input_type, value| next if value.nil? || value.empty? PROTECT_RULES.each do |_key, rule| protect_rule = Contrast::PROTECT.rule(rule[:rule_name]) logger.debug("Rule #{ rule[:rule_name] } not recognised in Protect rules") if protect_rule.nil? # check if rule is enabled next unless protect_rule&.enabled? # method tampering doesn't take value if rule[:rule_name] == Contrast::Agent::Protect::Rule::HttpMethodTampering::NAME rule[:classification].send(:classify, rule[:rule_name], input_type, input_analysis) else rule[:classification].send(:classify, rule[:rule_name], input_type, value, input_analysis) end end end input_analysis end # Extract the inputs from the request context and label them with Protect # input type tags. Each tag will contain one or more user inputs. # # This methods is to be expanded and modified as needed by other Protect rules # and sub-rules for their requirements. # # @param request [Contrast::Agent::Request] current request context. # @return inputs [Hash user_inputs>] def extract_input request inputs = {} inputs[BODY] = request.body inputs[COOKIE_NAME] = request.cookies.keys inputs[COOKIE_VALUE] = request.cookies.values inputs[HEADER] = request.headers inputs[PARAMETER_NAME] = request.parameters.keys inputs[PARAMETER_VALUE] = request.parameters.values inputs[QUERYSTRING] = request.query_string inputs[METHOD] = request.request_method extract_multipart(inputs, request) inputs.compact! inputs end # Extract the filename and name of the Content Disposition Header. # # @param inputs [Hash user_inputs>] # @param request [Contrast::Agent::Request] current request context. def extract_multipart inputs, request disposition = request.rack_request.env[DISPOSITION_KEYS[0]] disposition = request.rack_request.env[DISPOSITION_KEYS[1]] if disposition.nil? || disposition.empty? return unless disposition pairs = {} disposition.split(SEMICOLON).each do |elem| new_pair = elem.strip.split(EQUALS, 2) pairs[new_pair[0].downcase] = new_pair[1] if new_pair end inputs[MULTIPART_NAME] = pairs[DISPOSITION_NAME] inputs[MULTIPART_FIELD_NAME] = pairs[DISPOSITION_FILENAME] end end end end end end