# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'rack' require 'contrast/utils/hash_digest' require 'contrast/utils/duck_utils' require 'contrast/utils/string_utils' require 'contrast/agent/assess/rule/response/base_rule' module Contrast module Agent module Assess module Rule module Response # These rules check the content of the HTTP Response to determine if something was set incorrectly or # insecurely in it. module BodyRule protected HTML_PROP = 'html'.cs__freeze START_PROP = 'start'.cs__freeze END_PROP = 'end'.cs__freeze FORM_START_REGEXP = /<form/i.cs__freeze META_TYPE = 'META tag' PRAGMA = 'pragma' # Rules discern which responses they can/should analyze. # # @param response [Contrast::Agent::Response] the response of the application def analyze_response? response super && body?(response) end # Determine if a response has a body or not. # # @param response [Contrast::Agent::Response] the response of the application # @return [Boolean] def body? response Contrast::Utils::StringUtils.present?(response.body) end # Find the elements in this section, if any, so as to determine if they violate this rule. # # @param section [String,nil] html section to find element # @param element_start_str [String] element to find in html section # @return [Array<Hash>] the found elements of this section, as well as their start and end indexes. def html_elements section, element_start_str = '', capture_overflow: false return [] unless section return [] unless (potentials = potential_elements(section, element_start_str).flatten).any? elements = [] section_start = 0 potentials.each do |potential_element| next unless potential_element next unless element_openings.any? { |opening| potential_element.start_with?(opening) } start = section&.index(element_start_str, section_start) next if Contrast::Utils::DuckUtils.empty_duck?(start) stop = potential_element.index('>').to_i next if Contrast::Utils::DuckUtils.empty_duck?(stop) section_close = start + 6 + stop # Now we have valid tag section with start and stop. # Save new boundaries. This is to make sure that If # on previous iteration there were non valid section, # the start_section will be assigned to nil, thus making # the detection of new section not possible, and throwing # an error. To that end old values are kept safe. # # Assign new start index. section_start = start # Assign new end index. element_stop = stop elements << capture(section, section_start, section_close, element_stop, overflow: capture_overflow) section_start = section_close end elements end def potential_elements section, element_start section.split(element_start) end def element_openings [' ', "\n", "\r", "\t", '>'] end # Capture the information needed to build the properties of this finding by parsing out from the body # # @param body [String] the entire HTTP Response body # @param body_start [Integer] the start of the range to take from the body # @param body_close [Integer] the end of the range to take from the body # @param tag_stop [Integer] the index of the end of the html tag from its start # @return [Hash] def capture body, body_start, body_close, tag_stop, overflow: false tag = {} # Capture the 50 characters in front of the form, or up to the start if the form starts before 50. if overflow capture_start = body_start < 50 ? 0 : body_start - 50 # Start is where the '<form' is in the body # 6 accounts for the characters in the form and the opening char # potential_form.index('>') accounts for finding the rest of the form # 50 accounts for the context to capture beyond capture_close = body_close + 50 tag[HTML_PROP] = body[capture_start...capture_close] tag[START_PROP] = body_start < 50 ? body_start : 50 else tag[HTML_PROP] = body[body_start...body_close] tag[START_PROP] = body_start end tag[END_PROP] = tag[START_PROP] + 6 + tag_stop tag end end end end end end end