# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # Copyright (c) 2021 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' module Contrast module Agent module Assess module Rule module Response # These rules check that the HTTP Headers include CSP header types class CspHeaderInsecure < BaseRule def rule_id 'csp-header-insecure' end protected CSP_HEADERS = %w[CONTENT_SECURITY_POLICY X_CONTENT_SECURITY_POLICY X_WEBKIT_CSP].cs__freeze SETTINGS = %w[ base-uri child-src default-src connect-src frame-src media-src object-src script-src style-src form-action frame-ancestors plugin-types reflected-xss referer ].cs__freeze UNSAFE_VALUE_REGEXP = /^unsafe-(?:inline|eval)$/.cs__freeze ASTERISK_REGEXP = /[*]/.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 && headers?(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 [Contrast::Utils::ObjectShare::EMPTY_STRING, nil] if CSP Header is not found def violated? response settings = {} csp_hash = get_csp_header_values(response.headers) return if csp_hash.nil? SETTINGS.each do |setting_attr| value = csp_hash[setting_attr] key = convert_key(setting_attr) settings["#{ key }Secure"] = !value.nil? && value_secure?(value) && value_safe?(value) settings["#{ key }Value"] = value.nil? ? Contrast::Utils::ObjectShare::EMPTY_STRING : value end { DATA => settings } end # Get the CSP values from and transforms them to key value hash # # ex default-src 'self' *.test.com; img-src * becomes: # { default-src: "'self' *.test.com", img-src: "*" } # # @param headers [Hash] the response of the application # @return [Array, nil] array of CSP header values def get_csp_header_values headers csp_hash = {} CSP_HEADERS.each do |header_key| next unless headers[header_key]&.length&.positive? values = headers[header_key].split(Contrast::Utils::ObjectShare::SEMICOLON) values.each do |value| normalized = value.downcase.strip kv = normalized.split(Contrast::Utils::ObjectShare::SPACE, 2) csp_hash[kv[0]] = kv[1] end end csp_hash end def value_secure? value ASTERISK_REGEXP.match(value).nil? end def value_safe? value UNSAFE_VALUE_REGEXP.match(value).nil? end # Converts the CSP key to camelcase to be used as key for evidence object # # base-uri -> baseUri # # @param key [String] key as found in header # @return [String] camelcase key def convert_key key return key unless key.include?('-') str = key.split('-') "#{ str[0] }#{ str[1].capitalize }" end end end end end end end