# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/assess/rule/response/header_rule' require 'contrast/agent/assess/rule/response/body_rule' require 'contrast/utils/object_share' 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 < HeaderRule include BodyRule HEADER_KEYS = %w[Cache-Control].cs__freeze ACCEPTED_VALUES = [/no-store/, /no-cache/].cs__freeze DEFAULT_SAFE = false META_START_STR = //i.cs__freeze NAME = 'cache-control' def rule_id 'cache-controls-missing' end protected # 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 cache_header = cache_control_from(response) cache_meta = cache_meta_tags(response) has_header = cache_header && !cache_header.blank? has_meta = cache_meta.any? # Because we're not safe by default, the rule should never hit this case, but we'll handle it just in # case. return { DATA => Contrast::Utils::ObjectShare::EMPTY_ARRAY.to_json } unless has_header || has_meta evidence = [] # If we have a header tag, then we need to make sure it is set safely. If it is, there'll be no evidence # and we can return as the header prevents violation. if has_header header_evidence = header_evidence(cache_header) return unless header_evidence evidence << header_evidence end # If we have no header, or an unsafe header, then we need to check the meta tag to make sure it is set # safely. If it is, there'll be no evidence and we can return as the meta tag prevents violation. if has_meta tag_evidence = tag_evidence(cache_meta) return unless tag_evidence evidence << tag_evidence end # Otherwise, we'll report the violation. { DATA => evidence.to_json } end # @param response [Contrast::Agent::Response] the response of the application # @return [Array] def cache_meta_tags response html_elements(response.body&.split(HEAD_TAG)&.last, META_START_STR). select { |tag| cache_control_tag?(tag[HTML_PROP]) } end # Process Header value to determine if it violates rule # @param cache_control [String] the value of the Cache-Control header # @return [Hash, nil] the evidence hash or nil def header_evidence cache_control # If header is valid, then this portion of the rule isn't violated. return if valid_header?(cache_control) # evidence requires header value string, pull directly instead of rebuilding from hash evidence(HEADER_TYPE, NAME, cache_control) end # Process Body to determine if cache control meta tag violates rule # @param cache_meta_tags [Array] the meta tags which contain Cache-Control values # @return [Hash, nil] the evidence hash or nil def tag_evidence cache_meta_tags violation = cache_meta_tags.find { |tag| !safe_meta_cache_tag?(tag[HTML_PROP]) } violation ? evidence(META_TYPE, PRAGMA, violation[HTML_PROP]) : nil end def potential_elements section, element_start section.split(element_start) 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, /'cache-control'/i, /"cache-control"/i] end # @param tag [String] the tag to check # @return [Boolean] if the tag has cache-control settings or not def cache_control_tag? tag 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 accepted_http_values.any? { |el| (tag =~ el) == http_equiv_idx } 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 safe_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 accepted_values.any? { |value| (tag =~ value) == content_idx } end # This method accepts the violation and transforms it to the proper hash before returning a violation. # Unlike other rules, this returns a complex structure to be converted to JSON on reporting -- do NOT cast # it here as that'll result in extra escaping later. # # @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 # @return [Hash] def evidence type, name, value { 'type' => type, 'name' => name, 'value' => value } end # return the cache control value from the response, either as a Hash in later versions of Rails or as a # String in all other frameworks/ response types (remember, response can be a few things). # # @param response [Contrast::Agent::Response] # @return [String] def cache_control_from response control = if response.rack_response.cs__is_a?(Rack::Response) response.rack_response.cache_control else get_header_value(response) end control.cs__is_a?(Hash) ? cache_control_to_s(control) : control end # Rebuilds the String value of the Cache-Control Header from the hash build in the Rack::Response # # @param hsh [Hash] # @return [String] def cache_control_to_s hsh values = [] hsh.each_pair do |k, v| key = k.to_s.tr('_', '-') values << if key.to_sym == :extras v elsif v.is_a?(TrueClass) key else "#{ key }=#{ v }" end end values.join(', ') end end end end end end end