# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/utils/object_share' module Contrast module Agent module Protect module Rule # This module implements the sqli-worth-watching-v2 check to determine whether input # is IGNORED or WORTH_WATCHING. If WORTH_WATCHING => analyze at sink level. # https://protect-spec.prod.dotnet.contsec.com/rules/sql-injection.html#input-tracing module SqliWorthWatching COLOR_CODE = /^#[0-9A-Fa-f]{6}$/.cs__freeze ALPHA_NUMERIC_AND_SPACES = /^+[a-zA-Z\d\s]+$/.cs__freeze OR_CLAUSE = /[oO][rR]/.cs__freeze EXPLOITABLE_SUBSTRING = %w[# -- // /*].cs__freeze SUSPICIOUS_CHARS = %w[` " \' ; - % , ( ) | { } =].cs__freeze SQL_COMMENTS = %w[# /* */ // -- @@].cs__freeze BLOCK_START = '/*'.cs__freeze BLOCK_END = '*/'.cs__freeze SQL_KEYWORDS = %w[ alter begin between create case column_name current_user delete drop exec execute from group insert limit like merge order outfile select session_user syslogins update union UTL_INADDR UTL_HTTP ].cs__freeze # This method will determine if a user input is Worth watching and return true if it is. # This is done by running checks, and if the inputs is worth to watch it would be # saved for the later sink sqli input analysis. # # @param input [String] the user input to be inspected # @return true | false def sqli_worth_watching? input return false if input.nil? || input.empty? exploitable?(input) && ( input.match?(OR_CLAUSE) || sql_comments?(input) || suspicious_chars?(input) || language_keywords?(input) ) end private # Check if input is exploitable, with min length set to 3 chars # # @param input [String] the user input to be inspected # @return true | false def exploitable? input return false if input.length < 3 && input.match?(ALPHA_NUMERIC_AND_SPACES) return false if input.length == 3 && !contains_substring?(input, EXPLOITABLE_SUBSTRING) return false if input.length == 7 && input.match?(COLOR_CODE) true end # Check if input contains sqli comments: # '# /* */ // -- @@' # # @param input [String] the user input to be inspected # @return true | false def sql_comments? input input.length >= 3 && contains_substring?(input, SQL_COMMENTS) end # Check if input contains block comments starting and # ending with '/*..*/' # # @param input [String] the user input to be inspected # @return true | false def block_comments? input idx1 = input.index(BLOCK_START) idx2 = input.index(BLOCK_END) return false if idx1.nil? || idx2.nil? (idx1 >= 0 && idx2 >= 2 && (idx1 < idx2)) end # Runs the input against suspicious chars array. # # @param input [String] the user input to be inspected # @return true | false def suspicious_chars? input input.length >= 7 && (number_of_substrings(input, SUSPICIOUS_CHARS) >= 2 || block_comments?(input)) end # Runs the input against SQL language preserved words. # # @param input [String] the user input to be inspected # @return true | false def language_keywords? input contains_substring? input, SQL_KEYWORDS end # Helper method to find a substrings in given input. # # @param substrings [Array] set of substrings to inspect. # @return true | false def contains_substring? input, substrings return true if substrings.any? { |sub| input.include?(sub) } false end # Helper method to find the number of substrings. # # @param input [String] the user input to be inspected # @return number [Integer] Number of substrings def number_of_substrings input, substring number = 0 input.each_char.reduce(0) { |_acc, elem| number += 1 if contains_substring?(elem, substring) } number end end end end end end