# typed: ignore # Copyright (c) 2015 Sqreen. All Rights Reserved. # Please refer to our terms for more information: https://www.sqreen.com/terms.html require 'securerandom' require 'sqreen/rules/attrs' require 'sqreen/binding_accessor' require 'sqreen/rules/rule_cb' require 'sqreen/safe_json' require 'sqreen/exception' require 'sqreen/util/capper' require 'sqreen/dependency/libsqreen' require 'sqreen/encoding_sanitizer' module Sqreen module Rules class WAFCB < RuleCB # 2^30 -1 or 2^62 -1 MAX_FIXNUM = 1.size == 4 ? 1_073_741_823 : 4_611_686_018_427_387_903 # will be converted to a long, so better not to overflow INFINITE_BUDGET_US = MAX_FIXNUM def self.libsqreen? Sqreen::Dependency::LibSqreen.required? end def self.waf? Sqreen::Dependency.const_exist?('LibSqreen::WAF') end attr_reader :binding_accessors, :max_run_budget_us, :waf_rule_name def initialize(*args) super(*args) @overtimeable = false unless WAFCB.libsqreen? && WAFCB.waf? Sqreen.log.warn('libsqreen gem with waf not found') return end unless @data['values'] Sqreen.log.warn('no values in data') return end ::LibSqreen::WAF.logger = Sqreen.log name = format("%s_%s", SecureRandom.uuid, rule_name) unless @data['values']['waf_rules'] && (::LibSqreen::WAF[name] = @data['values']['waf_rules']) Sqreen.log.error("WAF rule #{name} failed to be set, from #<#{self.class.name}:0x#{object_id.to_s(16).rjust(16, '0')}>") return end @waf_rule_name = name Sqreen.log.debug("WAF rule #{name} set, from #<#{self.class.name}:0x#{object_id.to_s(16).rjust(16, '0')}>") @binding_accessors = @data['values'].fetch('binding_accessors', []).each_with_object({}) do |e, h| h[e] = BindingAccessor.new(e) end # 0 for using defaults (PW_RUN_TIMEOUT) @max_run_budget_us = (@data['values'].fetch('budget_in_ms', 0) * 1000).to_i @max_run_budget_us = INFINITE_BUDGET_US if @max_run_budget_us >= INFINITE_BUDGET_US Sqreen.log.debug { "Max WAF run budget for #{@waf_rule_name} set to #{@max_run_budget_us} us" } ObjectSpace.define_finalizer(self, WAFCB.finalizer(@waf_rule_name.dup)) end def pre(instance, args, budget) return unless WAFCB.libsqreen? && WAFCB.waf? request = framework.request return if !waf_rule_name || !request env = [binding, framework, instance, args] start = Sqreen.time if budget capper = Sqreen::Util::Capper.new(string_size_cap: 4096, size_cap: 150, depth_cap: 10) waf_args = binding_accessors.each_with_object({}) do |(e, b), h| h[e] = capper.call(b.resolve(*env)) end waf_args = Sqreen::EncodingSanitizer.sanitize(waf_args) if budget rem_budget_s = budget - (Sqreen.time - start) return advise_action(nil) if rem_budget_s <= 0.0 waf_gen_budget_us = [(rem_budget_s * 1_000_000).to_i, MAX_FIXNUM].min else # no budget waf_gen_budget_us = INFINITE_BUDGET_US end action, data = ::LibSqreen::WAF.run(waf_rule_name, waf_args, waf_gen_budget_us, @max_run_budget_us) case action when :monitor record_event({ 'waf_data' => data }) advise_action(nil) when :block record_event({ 'waf_data' => data }) advise_action(:raise) when :good advise_action(nil) when :timeout Sqreen.log.debug("WAF over time budget: #{action}") advise_action(nil) when :invalid_call Sqreen.log.debug("Error from waf: #{action}") advise_action(nil) raise Sqreen::WAFError.new(waf_rule_name, action, data, waf_args) when :invalid_rule, :invalid_flow, :no_rule Sqreen.log.debug("error from waf: #{action}") advise_action(nil) raise Sqreen::WAFError.new(waf_rule_name, action, data) else Sqreen.log.warn("unexpected action returned from waf") advise_action(nil) end end def self.finalizer(rule_name) lambda do |object_id| return unless WAFCB.libsqreen? ::LibSqreen::WAF.delete(waf_rule_name) Sqreen.log.debug("WAF rule #{rule_name} deleted, from #<#{name}:0x#{object_id.to_s(16).rjust(16, '0')}>") end end def record_exception(exception, infos = {}, at = Time.now.utc) infos.merge!(exception_to_infos(exception)) if exception.is_a?(Sqreen::WAFError) super(exception, infos, at) end private def exception_to_infos(e) { waf_rule: e.rule_name, error_code: ERROR_CODES[e.error], }.tap do |r| r[:error_data] = e.data if e.data r[:args] = e.args if e.args end end ERROR_CODES = { internal_error: -6, timeout: -5, invalid_call: -4, invalud_rule: -3, invalid_flow: -2, no_rule: -1, good: 0, monitor: 1, block: 2, }.freeze end end end