# 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/sql_sample_builder' require 'contrast/agent/reporting/input_analysis/input_type' require 'contrast/agent/protect/rule/no_sqli/no_sqli_input_classification' module Contrast module Agent module Protect module Rule # The Ruby implementation of the Protect NoSQL Injection rule. class NoSqli < Contrast::Agent::Protect::Rule::BaseService # Generate a sample for the No-SQL injection detection rule, allowing for reporting to and rendering # by TeamServer include SqlSampleBuilder::NoSqliSample # Defining build_attack_with_match method include SqlSampleBuilder::AttackBuilder include Contrast::Agent::Reporting::InputType NAME = 'nosql-injection' BLOCK_MESSAGE = 'NoSQLi rule triggered. Response blocked.' APPLICABLE_USER_INPUTS = [ BODY, COOKIE_NAME, COOKIE_VALUE, HEADER, PARAMETER_NAME, PARAMETER_VALUE, JSON_VALUE, MULTIPART_VALUE, MULTIPART_FIELD_NAME, XML_VALUE, DWR_VALUE ].cs__freeze def rule_name NAME end def block_message BLOCK_MESSAGE end def applicable_user_inputs APPLICABLE_USER_INPUTS end # NoSQLI input classification # # @return [module] def classification @_classification ||= Contrast::Agent::Protect::Rule::NoSqliInputClassification.cs__freeze end # @raise [Contrast::SecurityException] if the attack is blocked # raised with BLOCK_MESSAGE 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) raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if blocked? end def build_attack_with_match context, input_analysis_result, result, candidate_string, **kwargs return result if mode == :NO_ACTION || mode == :PERMIT result ||= build_attack_result(context) update_successful_attack_response(context, input_analysis_result, result, candidate_string) append_sample(context, input_analysis_result, result, candidate_string, **kwargs) result 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 protected def find_attacker context, potential_attack_string, **kwargs if potential_attack_string # We need the query hash to be a JSON string to match on JSON input attacks. # Before that we need to check if a string is already in json form. begin potential_attack_string = if json?(potential_attack_string) potential_attack_string else JSON.generate(potential_attack_string).to_s end rescue JSON::GeneratorError logger.trace('Error in JSON::generate', input: potential_attack_string) nil end end super(context, potential_attack_string, **kwargs) end # Check to see if a string is in JSON form. # # @return [Boolean] def json? string return true if JSON.parse(string) rescue StandardError false end end end end end end