# 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/agent/protect/rule/base' module Contrast module Agent module Protect module Rule # This class handles our implementation of the Untrusted # Deserialization Protect rule. class Deserialization < Contrast::Agent::Protect::Rule::Base # The TeamServer recognized name of this rule NAME = 'untrusted-deserialization' # The rule specific reason for raising a security exception. BLOCK_MESSAGE = 'Untrusted Deserialization rule triggered. Deserialization blocked.' # Gadgets that map to ERB modules ERB_GADGETS = %w[ object:ERB ].cs__freeze # Gadgets that map to ActionDispatch modules ACTION_DISPATCH_GADGETS = %w[ object:ActionDispatch::Routing::RouteSet::NamedRouteCollection ].cs__freeze # Gadgets that map to Arel Modules AREL_GADGETS = %w[ string:Arel::Nodes::SqlLiteral object:Arel::Nodes ].cs__freeze # Used to indicate to TeamServer the gadget is an ERB module ERB = 'ERB' # Used to indicate to TeamServer the gadget is an ActionDispatch # module DISPATCH = 'ActionDispatch' # Used to indicate to TeamServer the gadget is an Arel module AREL = 'Arel' # Return the TeamServer understood id / name of this rule. # @return [String] the TeamServer understood id / name of this rule. def name NAME end # Return the specific blocking message for this rule. # @return [String] the reason for the raised security exception. def block_message BLOCK_MESSAGE end # Per the spec, this rule applies regardless of input. Only the mode # of the rule and code exclusions apply at this point. # @return [Boolean] should the rule apply to this call. def infilter? _context return false unless enabled? return false if protect_excluded_by_code? true end # Determine if the input being deserialized is an attack and take # appropriate actions. # @param context [Contrast::Agent::RequestContext] the request # context in which this attack is occurring. # @param serialized_input [String] the string being deserialized. # @raise [Contrast::SecurityException] if attack detected while in # block mode. def infilter context, serialized_input return unless infilter?(context) gadget = find_gadget(serialized_input) # If there isn't a gadget, this isn't an attack, so we have nothing # to do here. Let the application carry on business as usual. return unless gadget ia_result = build_evaluation(serialized_input) kwargs = { GADGET_TYPE: gadget } result = build_attack_with_match(context, ia_result, nil, serialized_input, **kwargs) append_to_activity(context, result) raise Contrast::SecurityException.new(self, block_message) if blocked? end # Determine if the issued command was called while we're # deserializing. If we are, treat this as an attack. # @param gadget_command [String] the command being executed during # the deserialization call. # @raise [Contrast::SecurityException] if attack detected while in # block mode. def check_command_scope gadget_command # If we're within 'deserialization scope', then we've got a # deserialization method in our call stack. return unless in_deserialization_scope? context = Contrast::Agent::REQUEST_TRACKER.current kwargs = { COMMAND_SCOPE: true } ia_result = build_evaluation(gadget_command) result = build_attack_with_match(context, ia_result, nil, gadget_command, **kwargs) append_to_activity(context, result) raise Contrast::SecurityException.new(self, BLOCK_MESSAGE) if blocked? end protected # Build the RaspRuleSample for the detected Deserialization attack. # @param context [Contrast::Agent::RequestContext] the request # context in which this attack is occurring. # @param input_analysis_result [Contrast::Api::Settings::InputAnalysisResult] # the result of the analysis done by this rule. # @param _candidate_string [nil] unused. # @param kwargs [Hash, nil] Hash of inputs used by this rule to flesh # out the report to TeamServer in order to provide specific # information for this rule. # @return [Contrast::Api::Dtm::RaspRuleSample] the information needed # to render this attack event in TeamServer. def build_sample context, input_analysis_result, _candidate_string, **kwargs sample = build_base_sample(context, input_analysis_result) sample.untrusted_deserialization = Contrast::Api::Dtm::UntrustedDeserializationDetails.new deserializer = kwargs[:GADGET_TYPE] sample.untrusted_deserialization.deserializer = Contrast::Utils::StringUtils.protobuf_safe_string(deserializer) command = !!kwargs[:COMMAND_SCOPE] sample.untrusted_deserialization.command = command sample end private # Used to name this input since input analysis isn't done for this # rule INPUT_NAME = 'Serialized Gadget' # We know that this attack happened, so the result is always matched # and the level is always critical. Only variable is the Gadget # supplied by the attacker. # @param gadget_string [String] the input to be deserialized in which # the gadget exists or the command that resulted from deserializing # an input not detected in the initial infilter. # @return [Contrast::Api::Settings::InputAnalysisResult] the result # of the analysis done by this rule. def build_evaluation gadget_string ia_result = Contrast::Api::Settings::InputAnalysisResult.new ia_result.rule_id = name ia_result.input_type = :UNKNOWN ia_result.key = INPUT_NAME ia_result.value = Contrast::Utils::StringUtils.protobuf_safe_string(gadget_string) ia_result end # Find the gadget within the serialized input, if such a gadget # exists. # @param serialized_input [String] the string being deserialized in # which the gadget may exist. # @return [String, nil] the gadget, if found. def find_gadget serialized_input if ERB_GADGETS.any? { |gadget| serialized_input.index(gadget) } ERB elsif ACTION_DISPATCH_GADGETS.any? { |gadget| serialized_input.index(gadget) } DISPATCH elsif AREL_GADGETS.any? { |gadget| serialized_input.index(gadget) } AREL end end end end end end end