class SecureHeaders::CSP

Constants

DIRECTIVE_INJECTION_REGEX

Bytes that would let a caller-controlled value break out of its directive and inject sibling CSP directives. CR/LF are included so naive downstreams that split on bare r can’t be used to smuggle directives either.

Public Class Methods

new(config = nil) click to toggle source
# 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

name() click to toggle source

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
value() click to toggle source

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

append_nonce(source_list, nonce) click to toggle source

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
build_media_type_list_directive(directive) click to toggle source
# 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
build_report_to_directive(directive) click to toggle source
# 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
build_sandbox_list_directive(directive) click to toggle source
# 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
build_source_list_directive(directive) click to toggle source

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
build_value() click to toggle source

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
directives() click to toggle source

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
keep_wildcard_sources(source_list) click to toggle source

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
minify_source_list(directive, source_list) click to toggle source

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
normalize_uri_paths(source_list) click to toggle source
# 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
populate_nonces(directive, source_list) click to toggle source

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
reject_all_values_if_none(source_list) click to toggle source

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
scrub_directive_value(directive, value) click to toggle source

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
strip_source_schemes(source_list) click to toggle source

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
symbol_to_hyphen_case(sym) click to toggle source
# File lib/secure_headers/headers/content_security_policy.rb, line 244
def symbol_to_hyphen_case(sym)
  sym.to_s.tr("_", "-")
end