# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true cs__scoped_require 'contrast/core_extensions/protect/applies_sqli_rule' module Contrast module Agent module Protect module Rule # The Ruby implementation of the Protect SQL Injection rule. class Sqli < Contrast::Agent::Protect::Rule::BaseService NAME = 'sql-injection' BLOCK_MESSAGE = 'SQLi rule triggered. Response blocked.' def name NAME end def block_message BLOCK_MESSAGE end def infilter context, database, query_string return nil unless infilter?(context) result = find_attacker(context, query_string, database: database) return nil unless result append_to_activity(context, result) raise Contrast::SecurityException.new(self, BLOCK_MESSAGE) if blocked? end def build_attack_with_match context, input_analysis_result, result, query_string, **kwargs attack_string = input_analysis_result.value regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE) return nil unless query_string.match?(regexp) database = kwargs[:database] scanner = select_scanner(database) ss = StringScanner.new(query_string) length = attack_string.length while ss.scan_until(regexp) # the pos of StringScanner is at the end of the regexp (input string), # we need the beginning idx = ss.pos - attack_string.length last_boundary, boundary = scanner.crosses_boundary(query_string, idx, input_analysis_result.value) next unless last_boundary && boundary input_analysis_result.attack_count = input_analysis_result.attack_count + 1 kwargs[:start_idx] = idx kwargs[:end_idx] = idx + length kwargs[:boundary_overrun_idx] = boundary kwargs[:input_boundary_idx] = last_boundary result ||= build_attack_result(context) update_successful_attack_response(context, input_analysis_result, result, query_string) append_sample(context, input_analysis_result, result, query_string, **kwargs) end result end protected def build_sample context, input_analysis_result, candidate_string, **kwargs input = input_analysis_result.value sample = build_base_sample(context, input_analysis_result) sample.sqli = Contrast::Api::Dtm::SqlInjectionDetails.new sample.sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string) sample.sqli.start_idx = sample.sqli.query.index(input).to_i sample.sqli.end_idx = sample.sqli.start_idx + input.length sample.sqli.boundary_overrun_idx = kwargs[:boundary].to_i sample.sqli.input_boundary_idx = kwargs[:last_boundary].to_i sample end private def select_scanner database @sql_scanners ||= { Contrast::CoreExtensions::Protect::AppliesSqliRule::DATABASE_MYSQL => Contrast::Agent::Protect::Rule::Sqli::MysqlSqlScanner.new, Contrast::CoreExtensions::Protect::AppliesSqliRule::DATABASE_PG => Contrast::Agent::Protect::Rule::Sqli::PostgresSqlScanner.new, Contrast::CoreExtensions::Protect::AppliesSqliRule::DATABASE_SQLITE => Contrast::Agent::Protect::Rule::Sqli::SqliteSqlScanner.new }.cs__freeze @default_sql_scanner ||= Contrast::Agent::Protect::Rule::Sqli::DefaultSqlScanner.new @sql_scanners[database.to_s] || @default_sql_scanner end end end end end end