# 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