# Copyright (c) 2015 Sqreen. All Rights Reserved. # Please refer to our terms for more information: https://www.sqreen.io/terms.html require 'sqreen/parsers/sql' require 'strscan' module Sqreen module Detect class SQLInjection PARAM_SIZE_LIMIT = 0 def self.parser(db_type, db_infos) @parsers ||= {} res = nil results = @parsers[db_type] res = results.find { |infos, _| infos == db_infos } unless results.nil? return res.last unless res.nil? @parsers[db_type] ||= [] parser = Sqreen::Parsers::SQL.new(db_type, db_infos) @parsers[db_type] << [db_infos, parser] parser end attr_accessor :db_type, :db_infos def initialize(db_type, db_infos) @db_type = db_type @db_infos = db_infos @parser = SQLInjection.parser(db_type, db_infos) end # FIXME: we are likely to find false postive # As is, high risk of false positive with extremely short parameters. # E.g. if parameter value is 'e', it will be found in request, (e in # select) but not in literals. # # We may want to skip: # - too short parameters (e.g. < 10 letters?) # - non suspicious parameters (e.g. without blanks or comments?) def user_escape?(request, params) included = count_user_params_in_request(request, params) return false if included == {} escape_found = false # We found the user query inside the request. A risk exists. @parser.parse(request) literals = @parser.atoms.select(&:is_literal?) included.each do |param, expected_count| param_count = 0 literals.each do |literal| # Count number of literals that fully include the user query param_count += count_substring_nb(literal.val, param) end # puts "%s in raw request: %d, in atoms: %d" % [param, expected_count, param_count] next unless param_count != expected_count Sqreen.log.info format('injection for parameter %s', param.inspect) # require request aborption # log attack escape_found = true end escape_found end # What if a string can be prefixed itself? # E.g. substr = 'a b c a b c' # If str = 'a b c a b c a b c' we will return 2: # \----1----/ # \----2----/ def count_substring_nb(str, substr) s = StringScanner.new(str) nb = 0 quote = Regexp.quote(substr) re = Regexp.new(quote) nb += 1 while s.search_full(re, true, false) nb end def each_param_scalar(params, &block) case params when Hash then params.each { |_k, v| each_param_scalar(v, &block) } when Array then params.each { |v| each_param_scalar(v, &block) } else yield params end end # FIXME: this work on params values. We might wnat to work on parameters # names? High risk of false positive since a parameter name is often the # database column name. def count_user_params_in_request(request, params_hash) res = {} params_hash.each do |_type, params| next if params.nil? each_param_scalar(params) do |value| next unless value v = value.to_s next if v.size <= PARAM_SIZE_LIMIT next if v =~ /\A\.+\z/ next if v =~ /\A\s+\z/ next if v =~ /\A(\w+|\w[\w\.]+\w)\z/i # We need to overwrite the count of equal parameters that # came from different ways (e.g. Cookie and query). next if res.key? v nb = count_substring_nb(request, v) res[v] = nb if nb > 0 end end res end end end end