class SecureHeaders::ContentSecurityPolicy
Constants
- DIRECTIVE_INJECTION_REGEX
Bytes that would let a caller-controlled value break out of its directive and inject sibling
CSPdirectives. CR/LF are included so naive downstreams that split on bare r can't be used to smuggle directives either.
Public Class Methods
# File lib/secure_headers/headers/content_security_policy.rb, line 9 def initialize(config = nil) @config = if config.is_a?(Hash) if config[:report_only] ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG) else ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG) end elsif config.nil? ContentSecurityPolicyConfig.new(DEFAULT_CONFIG) else config end @preserve_schemes = @config[:preserve_schemes] @script_nonce = @config[:script_nonce] @style_nonce = @config[:style_nonce] end
Public Instance Methods
Returns the name to use for the header. Either “content-security-policy” or “content-security-policy-report-only”
# File lib/secure_headers/headers/content_security_policy.rb, line 31 def name @config.class.const_get(:HEADER_NAME) end
Return the value of the CSP header
# File lib/secure_headers/headers/content_security_policy.rb, line 37 def value @value ||= if @config build_value else DEFAULT_VALUE end end
Private Instance Methods
Private: adds a nonce or 'unsafe-inline' depending on browser support. If a nonce is populated, inline content is assumed.
While CSP is backward compatible in that a policy with a nonce will ignore unsafe-inline, this is more concise.
# File lib/secure_headers/headers/content_security_policy.rb, line 219 def append_nonce(source_list, nonce) if nonce source_list.push("'nonce-#{nonce}'") source_list.push(UNSAFE_INLINE) unless @config[:disable_nonce_backwards_compatibility] end source_list end
# File lib/secure_headers/headers/content_security_policy.rb, line 95 def build_media_type_list_directive(directive) return unless media_type_list = @config.directive_value(directive) if media_type_list && media_type_list.any? [ symbol_to_hyphen_case(directive), scrub_directive_value(directive, media_type_list.uniq.join(" ")) ].join(" ") end end
# File lib/secure_headers/headers/content_security_policy.rb, line 105 def build_report_to_directive(directive) return unless endpoint_name = @config.directive_value(directive) if endpoint_name && endpoint_name.is_a?(String) && !endpoint_name.empty? [symbol_to_hyphen_case(directive), scrub_directive_value(directive, endpoint_name)].join(" ") end end
# File lib/secure_headers/headers/content_security_policy.rb, line 72 def build_sandbox_list_directive(directive) return unless sandbox_list = @config.directive_value(directive) max_strict_policy = case sandbox_list when Array sandbox_list.empty? when true true else false end # A maximally strict sandbox policy is just the `sandbox` directive, # with no configuration values. if max_strict_policy symbol_to_hyphen_case(directive) elsif sandbox_list && sandbox_list.any? [ symbol_to_hyphen_case(directive), scrub_directive_value(directive, sandbox_list.uniq.join(" ")) ].join(" ") end end
Private: builds a string that represents one directive in a minified form.
directive_name - a symbol representing the various ALL_DIRECTIVES
Returns a string representing a directive.
# File lib/secure_headers/headers/content_security_policy.rb, line 139 def build_source_list_directive(directive) source_list = @config.directive_value(directive) if source_list != OPT_OUT && source_list && source_list.any? minified_source_list = minify_source_list(directive, source_list).join(" ") escaped_source_list = scrub_directive_value(directive, minified_source_list) [symbol_to_hyphen_case(directive), escaped_source_list].join(" ").strip end end
Private: converts the config object into a string representing a policy. Places default-src at the first directive and report-uri as the last. All others are presented in alphabetical order.
Returns a content security policy header value.
# File lib/secure_headers/headers/content_security_policy.rb, line 53 def build_value directives.map do |directive_name| case DIRECTIVE_VALUE_TYPES[directive_name] when :source_list, :require_sri_for_list, # require_sri is a simple set of strings that don't need to deal with symbol casing :require_trusted_types_for_list build_source_list_directive(directive_name) when :boolean symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name) when :sandbox_list build_sandbox_list_directive(directive_name) when :media_type_list build_media_type_list_directive(directive_name) when :report_to_endpoint build_report_to_directive(directive_name) end end.compact.join("; ") end
Private: return the list of directives, starting with default-src and ending with reporting directives (alphabetically ordered).
# File lib/secure_headers/headers/content_security_policy.rb, line 230 def directives [ DEFAULT_SRC, BODY_DIRECTIVES, REPORT_TO, REPORT_URI, ].flatten end
Discard trailing entries (excluding unsafe-*) since * accomplishes the same.
# File lib/secure_headers/headers/content_security_policy.rb, line 168 def keep_wildcard_sources(source_list) source_list.select { |value| WILDCARD_SOURCES.include?(value) } end
If a directive contains *, all other values are omitted. If a directive contains 'none' but has other values, 'none' is omitted. Schemes are stripped (see www.w3.org/TR/CSP2/#match-source-expression)
# File lib/secure_headers/headers/content_security_policy.rb, line 151 def minify_source_list(directive, source_list) source_list = source_list.compact if source_list.include?(STAR) keep_wildcard_sources(source_list) else source_list = populate_nonces(directive, source_list) source_list = reject_all_values_if_none(source_list) source_list = normalize_uri_paths(source_list) unless directive == REPORT_URI || @preserve_schemes source_list = strip_source_schemes(source_list) end source_list.uniq end end
# File lib/secure_headers/headers/content_security_policy.rb, line 181 def normalize_uri_paths(source_list) source_list.map do |source| # Normalize domains ending in a single / as without omitting the slash accomplishes the same. # https://www.w3.org/TR/CSP3/#match-paths § 6.6.2.10 Step 2 begin uri = URI(source) if uri.path == "/" next source.chomp("/") end rescue URI::InvalidURIError end if source.chomp("/").include?("/") source else source.chomp("/") end end end
Private: append a nonce to the script/style directories if script_nonce or style_nonce are provided.
# File lib/secure_headers/headers/content_security_policy.rb, line 203 def populate_nonces(directive, source_list) case directive when SCRIPT_SRC append_nonce(source_list, @script_nonce) when STYLE_SRC append_nonce(source_list, @style_nonce) else source_list end end
Discard any 'none' values if more directives are supplied since none may override values.
# File lib/secure_headers/headers/content_security_policy.rb, line 173 def reject_all_values_if_none(source_list) if source_list.length > 1 source_list.reject { |value| value == NONE } else source_list end end
Private: scrubs caller-controlled bytes that would let a value break out of its CSP directive (CR, LF, semicolon). Shared across every directive builder so sandbox / plugin-types / report-to / source-list all reject the same byte set with the same warn UX. Emits a single Kernel.warn per directive even when multiple offending bytes are present.
# File lib/secure_headers/headers/content_security_policy.rb, line 124 def scrub_directive_value(directive, value) str = value.to_s if str =~ DIRECTIVE_INJECTION_REGEX Kernel.warn("#{directive} contains a #{$~[0].inspect} in #{str.inspect} which will raise an error in future versions. It has been replaced with a blank space.") str.gsub(DIRECTIVE_INJECTION_REGEX, " ") else str end end
Private: Remove scheme from source expressions.
# File lib/secure_headers/headers/content_security_policy.rb, line 240 def strip_source_schemes(source_list) source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") } end
# File lib/secure_headers/headers/content_security_policy.rb, line 244 def symbol_to_hyphen_case(sym) sym.to_s.tr("_", "-") end