# Copyright (c) 2020 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 service. module SSRFValidator SSRF_RULE = 'ssrf' URL_PATTERN = %r{(?http|https|ftp|sftp|telnet|gopher|rtsp|rtsps|ssh|svn)://(?[^/?]+)(?/?[^?]*)(?\?.*)?}i.cs__freeze # 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 def self.valid? patcher, _object, _ret, args return true unless SSRF_RULE == patcher&.rule_id 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 querystring, then the entire URL is # dangerous for the User to control. start = match.begin(:protocol) finish = match.begin(:query_string) finish ||= url.length args[0].cs__properties.any_tags_between?(start, finish) end # Some SSRF triggers take multiple parameters to build the URL. # For the net_http_# sources, if the first parameter is a String, # the URL is built by appending the first and second parameters. def self.composite? patcher, args id = patcher.id.to_s return false unless id.start_with?(PATH_ONLY_PATCH_MARKER) args[0].is_a?(String) end def self.build_single_source args return nil unless args[0].cs__respond_to?(:cs__properties) args[0].to_s end def self.build_composite_source args if args.length > 1 return nil unless args[0].cs__respond_to?(:cs__properties) || args[1].cs__respond_to?(:cs__properties) args[0] + args[1] else return nil unless args[0].cs__respond_to?(:cs__properties) args[0].to_s end end end end end end end end