# 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/policy/applies_sqli_rule' require 'contrast/agent/protect/rule/sql_sample_builder' require 'contrast/agent/reporting/input_analysis/input_type' require 'contrast/agent/protect/rule/sqli/sqli_base_rule' require 'contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions' module Contrast module Agent module Protect module Rule # The Ruby implementation of the Protect SQL Injection rule. class Sqli < Contrast::Agent::Protect::Rule::SqliBaseRule # Generate a sample for the SQLI injection detection rule, allowing for reporting to and rendering # by TeamServer include SqlSampleBuilder::SqliSample # Defining build_attack_with_match method include SqlSampleBuilder::AttackBuilder include Contrast::Agent::Reporting::InputType class << self include Contrast::Agent::Reporting::InputType end NAME = 'sql-injection' SUB_RULES = [Contrast::Agent::Protect::Rule::SqliDangerousFunctions.new].cs__freeze def rule_name NAME end def block_message BLOCK_MESSAGE end def sub_rules SUB_RULES end def applicable_user_inputs APPLICABLE_USER_INPUTS end def infilter context, database, query_string return unless infilter?(context) result = find_attacker(context, query_string, database: database) return unless result append_to_activity(context, result) cef_logging(result, :successful_attack, value: query_string) raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if blocked? end # Allows for the InputAnalysis from Agent Library to be extracted early # @param context [Contrast::Agent::RequestContext] # @param potential_attack_string [String, nil] # @param ia_results [Array] # @param **kwargs # @return [Contrast::Agent::Reporting, nil] def find_attacker_with_results context, potential_attack_string, ia_results, **kwargs logger.trace('Checking vectors for attacks', rule: rule_name, input: potential_attack_string) result = nil ia_results.each do |ia_result| if potential_attack_string idx = potential_attack_string.index(ia_result.value) next unless idx database_type = kwargs[:database].to_sym input_length = ia_result.value.length lib_result = check_sql_input_with_agent(potential_attack_string, database_type, idx, input_length) kwargs[:result_struct] = lib_result result = build_attack_with_match(context, ia_result, result, potential_attack_string, **kwargs) else result = build_attack_without_match(context, ia_result, result, **kwargs) end end result end # We'll need a second place, where we need to check the token boundaries if are being crossed and # worth-watching. # # @param sql[String] SQL coming from parameter # @param database[String] Type of database # @param input_index[String] index in the sqlQuery string where user input was found # @param input_length[Number] length of the input value # @return [Hash, Boolean] def check_sql_input_with_agent sql, database, input_index, input_length return false unless (agent_lib = Contrast::AGENT_LIB) && sql && database agent_lib.check_sql_query(input_index, input_length, database, sql) end # @param context [Contrast::Agent::RequestContext] def infilter? context return false unless enabled? return false unless context&.agent_input_analysis&.results&.any? do |result| result.rule_id == rule_name end return false if protect_excluded_by_code? true end end end end end end