# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true module Contrast module Agent module Assess module Policy module TriggerValidation # Validator used to assert a SSRF finding is actually vulnerable # before serializing that finding as a DTM to report to the TeamServer. module SSRFValidator RULE_NAME = 'ssrf' URL_PATTERN = %r{(?http|https|ftp|sftp|telnet|gopher|rtsp|rtsps|ssh|svn)://(?[^/?]+)(?/?[^?]*)(?\?.*)?}i.cs__freeze # rubocop:disable Layout/LineLength # The Net::HTTP class validates host format on instantiation. Since # our triggers for that class are on the instance, they already # have this validation done for them. We do not need to apply the # validation in this case. PATH_ONLY_PATCH_MARKER = 'Assess:Trigger:Net::HTTP#' # A finding is valid for SSRF if the source of the trigger event is # a valid URL in which the User controls a section prior to the # querystring # https://bitbucket.org/contrastsecurity/assess-specifications/src/master/rules/dataflow/server_side_request_forgery.md # # @param patcher [Contrast::Agent::Patcher] the patcher instance # @param _object [Object] the object that was called # @param _ret [Object] the return value of the method # @param args [Array] the arguments passed to the method # @return [Boolean] true if the finding is valid, false otherwise def self.valid? patcher, _object, _ret, args return true if patcher.id.to_s.start_with?(PATH_ONLY_PATCH_MARKER) url = args[0].to_s match = url.match(URL_PATTERN) return false unless match # It is dangerous for the user to control a section of the URL # between the start of the protocol and the beginning of the # querystring. If there is no path, then the entire URL is # dangerous for the User to control. start = match.begin(:protocol) finish = match.begin(:path) finish ||= url.length properties = Contrast::Agent::Assess::Tracker.properties(args[0]) properties&.any_tags_between?(start, finish) end end end end end end end