# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/assess/rule/response/base_rule' require 'contrast/utils/string_utils' require 'json' module Contrast module Agent module Assess module Rule module Response # These rules check the content of the HTTP Response to determine if the body or the headers include and/or # set incorrectly the cache-control header class Cachecontrol < BaseRule def rule_id 'cache-controls-missing' end protected HEADER_KEY = 'Cache-Control'.cs__freeze ACCEPTED_VALUES = %w[no-store no-cache].cs__freeze # 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 the Response violates the Rule or not. If it does, return the evidence that proves it so. # # @param response [Contrast::Agent::Response] the response of the application # @return [Hash, nil] the evidence required to prove the violation of the rule def violated? response # This rule is violated if the header is not there # or if it's there, but the value is not 'no-store' or 'no-cache' headers = response.headers header_key_sym = HEADER_KEY.to_sym cache_control = headers[HEADER_KEY] || headers[header_key_sym] if cache_control && !valid_header?(cache_control) return to_cachecontrol_rule('header', 'cache-control', cache_control) end body = response.body # check if the meta tag is include it tags = meta_tags(body) tags.each do |tag| return to_cachecontrol_rule('meta', 'pragma', tag[HTML_PROP]) if meta_cache_tag? tag[HTML_PROP] end # we should return if header not presented and no tags are detected return {} if !(headers.key?(HEADER_KEY) || headers.key?(header_key_sym)) && tags.empty? nil end def valid_header? header ACCEPTED_VALUES.any? { |val| header.include?(val) || header == val } end # Find the tags in this body, if any, so as to determine if they violate this rule. # # @param body [String,nil] # @return [Array] the tags of this body, as well as their start and end indexes. def meta_tags body tags = [] body_start = 0 # meta tags are stored in the section head_section = body&.split(head_tag) return [] unless head_section potential_tags = head_section.map { |el| el.split(meta_start) } potential_tags.flatten.each do |potential_tag| next unless potential_tag next unless tag_openings.any? { |opening| potential_tag.starts_with?(opening) } body_start = body.index(meta_start, body_start) next unless body_start tag_stop = potential_tag.index('>').to_i next unless tag_stop body_close = body_start + 6 + tag_stop tags << capture(body, body_start, body_close, tag_stop) body_start = body_close end tags end def meta_start //i end def tag_openings [' ', "\n", "\r", "\t"] end def accepted_http_values [/'cache-control'/i, /"cache-control"/i] end def accepted_values [/'no-cache'/i, /"no-cache"/i, /"no-store"/i, /'no-store'/i] end # Determine if the given metatag does not have a valid cache-control tag. # Meta tags has the option to set http-equiv and content to set the http response header # to define for the document # # @param tag [String] the meta tag # @return [Boolean, nil] def meta_cache_tag? tag # Here we should determine the index of the needed keys # http-equiv and content http_equiv_idx = tag =~ /http-equiv=/i return false unless http_equiv_idx content_idx = tag =~ /content=/i return false unless content_idx # determine the value of the http-equiv if it's cache-control http_equiv_idx += 11 is_valid = accepted_http_values.any? { |el| (tag =~ el) == http_equiv_idx } return false unless is_valid content_idx += 8 return false if accepted_values.any? { |value| (tag =~ value) == content_idx } true end # This method allows to change the evidence we attach and the way we attach it # Change it accordingly the rule you work on # # @param evidence [Hash] the properties required to build this finding. # @param finding [Contrast::Api::Dtm::Finding] finding to attach the evidence to def build_evidence evidence, finding evidence.each_pair do |key, value| finding.properties[key] = Contrast::Utils::StringUtils.protobuf_format(value) end end # This method accepts the violation and transforms it to the proper hash # before return in as violation # # @param type [String] String of Header or META of the type # @param name [String] String of either cache-control or pragma # @param value [String] String of the violated value def to_cachecontrol_rule type, name, value { data: { type: type, name: name, value: value }.to_s } 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 # In this situation we don't need to capture before and after the meta tag, as this may produce an error # So if we capture 30-50 chars before and after the tag, we may capture part of the tag, we want to # inspect and eventually this wil return wrong string. Because of that - we split the and take # each meta tag and examine it tag = {} # we dont need to capture here before or after the meta tag tag[HTML_PROP] = body[body_start...body_close] tag[START_PROP] = body_start tag[END_PROP] = tag[START_PROP] + 6 + tag_stop tag end end end end end end end