# 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/header_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 < HeaderRule HEADER_KEYS = %w[Content-Security-Policy X-Content-Security-Policy X-Webkit-CSP].cs__freeze DEFAULT_SAFE = false 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 SAFE_REFLECTED_XSS = /1/.cs__freeze def rule_id 'csp-header-insecure' 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 [Contrast::Utils::ObjectShare::EMPTY_STRING, nil] if CSP Header is not found def violated? response settings = {} csp_hash = get_header_value(response) return if csp_hash.nil? SETTINGS.each do |setting_attr| # default src has to be checked all other keys may be missing next unless csp_hash.key?(setting_attr) || setting_attr == 'default-src' 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 evidence(settings.to_json) if settings.value?(false) 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_header_value response csp_hash = {} headers = response.headers HEADER_KEYS.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? || !SAFE_REFLECTED_XSS.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