# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true cs__scoped_require 'rack' cs__scoped_require 'contrast/utils/object_share' cs__scoped_require 'contrast/agent/request' cs__scoped_require 'contrast/agent/response' cs__scoped_require 'contrast/utils/stack_trace_utils' cs__scoped_require 'contrast/utils/timer' module Contrast module Agent module Protect module Rule # The Ruby implementation of the Protect Cross-Site Request Forgery # rule. class Csrf < Contrast::Agent::Protect::Rule::Base NAME = 'csrf' def name NAME end def stream_safe? false end def request_evaluator @_request_evaluator ||= Contrast::Agent::Protect::Rule::Csrf::CsrfEvaluator.new end def token_injector @_token_injector ||= Contrast::Agent::Protect::Rule::Csrf::CsrfTokenInjector.new end BLOCK_MESSAGE = 'CSRF rule triggered. Request blocked.' def prefilter context return unless enabled? && mode != :NO_ACTION request = context.request return if request_evaluator.can_ignore_check?(request) parameters = request.parameters return unless parameters&.any? tokens = parameters[token_injector.token_name] expected_token = token_injector.get_expected_token(context) return if valid_token?(expected_token, tokens) # unexpected token is interpreted as an attack with a match ia_result = Contrast::Api::Settings::InputAnalysisResult.new ia_result.input_type = :BODY ia_result.value = build_attack_string(context.request) result = build_attack_with_match( context, ia_result, nil, expected_token, details: build_details(expected_token, tokens)) append_to_activity(context, result) raise Contrast::SecurityException.new(self, BLOCK_MESSAGE) if blocked? end def valid_token? expected, actual_tokens actual_tokens&.include?(expected) end def postfilter context return unless enabled? && POSTFILTER_MODES.include?(mode) token_injector.do_injection(context) end def build_sample context, evaluation, _url, **kwargs sample = build_base_sample(context, evaluation) sample.csrf = kwargs[:details] sample end # Build a subclass of the RaspRuleSample using the query string and the # evaluation def build_details expected, actual_tokens details = Contrast::Api::Dtm::CsrfDetails.new details.name = Contrast::Utils::StringUtils.protobuf_safe_string(token_injector.token_name) details.expected = Contrast::Utils::StringUtils.protobuf_safe_string(expected) presented = build_presented_token(actual_tokens) details.presented = Contrast::Utils::StringUtils.protobuf_safe_string(presented) details end def build_presented_token actual_tokens return Contrast::Utils::ObjectShare::EMPTY_STRING unless actual_tokens&.any? actual_tokens.first end # Convert the request parameters into an attack string def build_attack_string request arr = [] request.dtm.normalized_request_params.each_with_index do |(name, value), index| arr << Contrast::Utils::ObjectShare::AND unless index.zero? arr += [name.to_s, Contrast::Utils::ObjectShare::EQUALS, value.values.to_s] end if arr.empty? request.query_string else arr.join end end end end end end end