Class: SecureHeaders::ContentSecurityPolicy

Inherits:
Object
  • Object
show all
Defined in:
lib/secure_headers/headers/content_security_policy.rb

Constant Summary collapse

MODERN_BROWSERS =
%w(Chrome Opera Firefox)
DEFAULT_VALUE =
"default-src https:".freeze
DEFAULT_CONFIG =
{ default_src: %w(https:) }.freeze
HEADER_NAME =
"Content-Security-Policy".freeze
REPORT_ONLY =
"Content-Security-Policy-Report-Only".freeze
HEADER_NAMES =
[HEADER_NAME, REPORT_ONLY]
DATA_PROTOCOL =
"data:".freeze
BLOB_PROTOCOL =
"blob:".freeze
SELF =
"'self'".freeze
NONE =
"'none'".freeze
STAR =
"*".freeze
UNSAFE_INLINE =
"'unsafe-inline'".freeze
UNSAFE_EVAL =
"'unsafe-eval'".freeze
DEPRECATED_SOURCE_VALUES =

leftover deprecated values that will be in common use upon upgrading.

[SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze
DEFAULT_SRC =
:default_src
CONNECT_SRC =
:connect_src
FONT_SRC =
:font_src
FRAME_SRC =
:frame_src
IMG_SRC =
:img_src
MEDIA_SRC =
:media_src
OBJECT_SRC =
:object_src
SANDBOX =
:sandbox
SCRIPT_SRC =
:script_src
STYLE_SRC =
:style_src
REPORT_URI =
:report_uri
DIRECTIVES_1_0 =
[
  DEFAULT_SRC,
  CONNECT_SRC,
  FONT_SRC,
  FRAME_SRC,
  IMG_SRC,
  MEDIA_SRC,
  OBJECT_SRC,
  SANDBOX,
  SCRIPT_SRC,
  STYLE_SRC,
  REPORT_URI
].freeze
BASE_URI =
:base_uri
CHILD_SRC =
:child_src
FORM_ACTION =
:form_action
FRAME_ANCESTORS =
:frame_ancestors
PLUGIN_TYPES =
:plugin_types
DIRECTIVES_2_0 =
[
  DIRECTIVES_1_0,
  BASE_URI,
  CHILD_SRC,
  FORM_ACTION,
  FRAME_ANCESTORS,
  PLUGIN_TYPES
].flatten.freeze
MANIFEST_SRC =

All the directives currently under consideration for CSP level 3. https://w3c.github.io/webappsec/specs/CSP2/

:manifest_src
REFLECTED_XSS =
:reflected_xss
DIRECTIVES_3_0 =
[
  DIRECTIVES_2_0,
  MANIFEST_SRC,
  REFLECTED_XSS
].flatten.freeze
BLOCK_ALL_MIXED_CONTENT =

All the directives that are not currently in a formal spec, but have been implemented somewhere.

:block_all_mixed_content
UPGRADE_INSECURE_REQUESTS =
:upgrade_insecure_requests
DIRECTIVES_DRAFT =
[
  BLOCK_ALL_MIXED_CONTENT,
  UPGRADE_INSECURE_REQUESTS
].freeze
SAFARI_DIRECTIVES =
DIRECTIVES_1_0
FIREFOX_UNSUPPORTED_DIRECTIVES =
[
  BLOCK_ALL_MIXED_CONTENT,
  CHILD_SRC,
  PLUGIN_TYPES
].freeze
FIREFOX_DIRECTIVES =
(
  DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES
).freeze
CHROME_DIRECTIVES =
(
  DIRECTIVES_2_0 + DIRECTIVES_DRAFT
).freeze
ALL_DIRECTIVES =
[DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort
BODY_DIRECTIVES =

Think of default-src and report-uri as the beginning and end respectively, everything else is in between.

ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
VARIATIONS =
{
  "Chrome" => CHROME_DIRECTIVES,
  "Opera" => CHROME_DIRECTIVES,
  "Firefox" => FIREFOX_DIRECTIVES,
  "Safari" => SAFARI_DIRECTIVES,
  "Other" => CHROME_DIRECTIVES
}.freeze
OTHER =
"Other".freeze
DIRECTIVE_VALUE_TYPES =
{
  BASE_URI                  => :source_list,
  BLOCK_ALL_MIXED_CONTENT   => :boolean,
  CHILD_SRC                 => :source_list,
  CONNECT_SRC               => :source_list,
  DEFAULT_SRC               => :source_list,
  FONT_SRC                  => :source_list,
  FORM_ACTION               => :source_list,
  FRAME_ANCESTORS           => :source_list,
  FRAME_SRC                 => :source_list,
  IMG_SRC                   => :source_list,
  MANIFEST_SRC              => :source_list,
  MEDIA_SRC                 => :source_list,
  OBJECT_SRC                => :source_list,
  PLUGIN_TYPES              => :source_list,
  REFLECTED_XSS             => :string,
  REPORT_URI                => :source_list,
  SANDBOX                   => :string,
  SCRIPT_SRC                => :source_list,
  STYLE_SRC                 => :source_list,
  UPGRADE_INSECURE_REQUESTS => :boolean
}.freeze
CONFIG_KEY =
:csp
STAR_REGEXP =
Regexp.new(Regexp.escape(STAR))
HTTP_SCHEME_REGEX =
%r{\Ahttps?://}
WILDCARD_SOURCES =
[
  UNSAFE_EVAL,
  UNSAFE_INLINE,
  STAR,
  DATA_PROTOCOL,
  BLOB_PROTOCOL
].freeze
META_CONFIGS =
[
  :report_only,
  :preserve_schemes
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config = nil, user_agent = OTHER) ⇒ ContentSecurityPolicy

Returns a new instance of ContentSecurityPolicy.



285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/secure_headers/headers/content_security_policy.rb', line 285

def initialize(config = nil, user_agent = OTHER)
  config = Configuration.deep_copy(DEFAULT_CONFIG) unless config
  @config = config
  @parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
    user_agent
  else
    UserAgent.parse(user_agent)
  end
  @report_only = @config[:report_only]
  @preserve_schemes = @config[:preserve_schemes]
  @script_nonce = @config[:script_nonce]
  @style_nonce = @config[:style_nonce]
end

Class Method Details

.combine_policies(original, additions) ⇒ Object

Public: combine the values from two different configs.

original - the main config additions - values to be merged in

raises an error if the original config is OPT_OUT

  1. for non-source-list values (report_only, block_all_mixed_content, upgrade_insecure_requests), additions will overwrite the original value.
  2. if a value in additions does not exist in the original config, the default-src value is included to match original behavior.
  3. if a value in additions does exist in the original config, the two values are joined.


208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/secure_headers/headers/content_security_policy.rb', line 208

def combine_policies(original, additions)
  if original == OPT_OUT
    raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
  end

  original = original.dup if original.frozen?

  # in case we would be appending to an empty directive, fill it with the default-src value
  additions.keys.each do |directive|
    unless original[directive] || !source_list?(directive)
      original[directive] = original[:default_src]
    end
  end

  # merge the two hashes. combine (instead of overwrite) the array values
  # when each hash contains a value for a given key.
  original.merge(additions) do |directive, lhs, rhs|
    if source_list?(directive)
      (lhs.to_a + rhs.to_a).compact.uniq
    else
      rhs
    end
  end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
end

.idempotent_additions?(config, additions) ⇒ Boolean

Public: determine if merging additions will cause a change to the actual value of the config.

e.g. config = { script_src: %w(example.org google.com)} and additions = { script_src: %w(google.com)} then idempotent_additions? would return because google.com is already in the config.

Returns:

  • (Boolean)


190
191
192
193
# File 'lib/secure_headers/headers/content_security_policy.rb', line 190

def idempotent_additions?(config, additions)
  return false if config == OPT_OUT
  config.to_s == combine_policies(config, additions).to_s
end

.make_header(config, user_agent) ⇒ Object

Public: generate a header name, value array that is user-agent-aware.

Returns a default policy if no configuration is provided, or a header name and value based on the config.



163
164
165
166
# File 'lib/secure_headers/headers/content_security_policy.rb', line 163

def make_header(config, user_agent)
  header = new(config, user_agent)
  [header.name, header.value]
end

.validate_config!(config) ⇒ Object

Public: Validates each source expression.

Does not validate the invididual values of the source expression (e.g. script_src => htt*p: will not raise an exception)



172
173
174
175
176
177
178
179
180
181
182
# File 'lib/secure_headers/headers/content_security_policy.rb', line 172

def validate_config!(config)
  return if config.nil? || config == OPT_OUT
  raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src]
  config.each do |key, value|
    if META_CONFIGS.include?(key)
      raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
    else
      validate_directive!(key, value)
    end
  end
end

Instance Method Details

#name ⇒ Object

Returns the name to use for the header. Either "Content-Security-Policy" or "Content-Security-Policy-Report-Only"



302
303
304
305
306
307
308
# File 'lib/secure_headers/headers/content_security_policy.rb', line 302

def name
  if @report_only
    REPORT_ONLY
  else
    HEADER_NAME
  end
end

#value ⇒ Object

Return the value of the CSP header



312
313
314
315
316
317
318
# File 'lib/secure_headers/headers/content_security_policy.rb', line 312

def value
  @value ||= if @config
    build_value
  else
    DEFAULT_VALUE
  end
end