secure_headers 5.2.0 → 6.0.0.alpha01

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of secure_headers might be problematic. Click here for more details.

Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +8 -4
  3. data/CHANGELOG.md +3 -7
  4. data/Gemfile +1 -1
  5. data/docs/upgrading-to-6-0.md +50 -0
  6. data/lib/secure_headers.rb +14 -76
  7. data/lib/secure_headers/configuration.rb +114 -164
  8. data/lib/secure_headers/headers/clear_site_data.rb +1 -3
  9. data/lib/secure_headers/headers/content_security_policy.rb +4 -17
  10. data/lib/secure_headers/headers/content_security_policy_config.rb +3 -13
  11. data/lib/secure_headers/headers/expect_certificate_transparency.rb +2 -3
  12. data/lib/secure_headers/headers/policy_management.rb +12 -11
  13. data/lib/secure_headers/headers/public_key_pins.rb +2 -3
  14. data/lib/secure_headers/headers/referrer_policy.rb +2 -2
  15. data/lib/secure_headers/headers/strict_transport_security.rb +2 -2
  16. data/lib/secure_headers/headers/x_content_type_options.rb +2 -2
  17. data/lib/secure_headers/headers/x_download_options.rb +2 -2
  18. data/lib/secure_headers/headers/x_frame_options.rb +1 -2
  19. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +2 -2
  20. data/lib/secure_headers/headers/x_xss_protection.rb +3 -3
  21. data/secure_headers.gemspec +1 -1
  22. data/spec/lib/secure_headers/configuration_spec.rb +15 -70
  23. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +12 -22
  24. data/spec/lib/secure_headers/headers/policy_management_spec.rb +33 -7
  25. data/spec/lib/secure_headers/middleware_spec.rb +7 -1
  26. data/spec/lib/secure_headers/view_helpers_spec.rb +1 -0
  27. data/spec/lib/secure_headers_spec.rb +39 -40
  28. data/spec/spec_helper.rb +7 -3
  29. metadata +5 -4
@@ -11,13 +11,11 @@ module SecureHeaders
11
11
  EXECUTION_CONTEXTS = "executionContexts".freeze
12
12
  ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECUTION_CONTEXTS]
13
13
 
14
- CONFIG_KEY = :clear_site_data
15
-
16
14
  class << self
17
15
  # Public: make an Clear-Site-Data header name, value pair
18
16
  #
19
17
  # Returns nil if not configured, returns header name and value if configured.
20
- def make_header(config = nil)
18
+ def make_header(config = nil, user_agent = nil)
21
19
  case config
22
20
  when nil, OPT_OUT, []
23
21
  # noop
@@ -13,6 +13,7 @@ module SecureHeaders
13
13
  FALLBACK_VERSION = ::UserAgent::Version.new("0")
14
14
 
15
15
  def initialize(config = nil, user_agent = OTHER)
16
+ user_agent ||= OTHER
16
17
  @config = if config.is_a?(Hash)
17
18
  if config[:report_only]
18
19
  ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
@@ -138,14 +139,8 @@ module SecureHeaders
138
139
  end
139
140
 
140
141
  if source_list != OPT_OUT && source_list && source_list.any?
141
- minified_source_list = minify_source_list(directive, source_list).join(" ")
142
-
143
- if minified_source_list =~ /(\n|;)/
144
- Kernel.warn("#{directive} contains a #{$1} in #{minified_source_list.inspect} which will raise an error in future versions. It has been replaced with a blank space.")
145
- end
146
-
147
- escaped_source_list = minified_source_list.gsub(/[\n;]/, " ")
148
- [symbol_to_hyphen_case(directive), escaped_source_list].join(" ").strip
142
+ normalized_source_list = minify_source_list(directive, source_list)
143
+ [symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
149
144
  end
150
145
  end
151
146
 
@@ -218,11 +213,7 @@ module SecureHeaders
218
213
  # unsafe-inline, this is more concise.
219
214
  def append_nonce(source_list, nonce)
220
215
  if nonce
221
- if nonces_supported?
222
- source_list << "'nonce-#{nonce}'"
223
- else
224
- source_list << UNSAFE_INLINE
225
- end
216
+ source_list.push("'nonce-#{nonce}'", UNSAFE_INLINE)
226
217
  end
227
218
 
228
219
  source_list
@@ -262,10 +253,6 @@ module SecureHeaders
262
253
  end
263
254
  end
264
255
 
265
- def nonces_supported?
266
- @nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
267
- end
268
-
269
256
  def symbol_to_hyphen_case(sym)
270
257
  sym.to_s.tr("_", "-")
271
258
  end
@@ -2,7 +2,6 @@
2
2
  module SecureHeaders
3
3
  module DynamicConfig
4
4
  def self.included(base)
5
- base.send(:attr_writer, :modified)
6
5
  base.send(:attr_reader, *base.attrs)
7
6
  base.attrs.each do |attr|
8
7
  base.send(:define_method, "#{attr}=") do |value|
@@ -42,7 +41,6 @@ module SecureHeaders
42
41
  @upgrade_insecure_requests = nil
43
42
 
44
43
  from_hash(hash)
45
- @modified = false
46
44
  end
47
45
 
48
46
  def update_directive(directive, value)
@@ -55,12 +53,10 @@ module SecureHeaders
55
53
  end
56
54
  end
57
55
 
58
- def modified?
59
- @modified
60
- end
61
-
62
56
  def merge(new_hash)
63
- ContentSecurityPolicy.combine_policies(self.to_h, new_hash)
57
+ new_config = self.dup
58
+ new_config.send(:from_hash, new_hash)
59
+ new_config
64
60
  end
65
61
 
66
62
  def merge!(new_hash)
@@ -109,17 +105,12 @@ module SecureHeaders
109
105
  def write_attribute(attr, value)
110
106
  value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
111
107
  attr_variable = "@#{attr}"
112
- prev_value = self.instance_variable_get(attr_variable)
113
108
  self.instance_variable_set(attr_variable, value)
114
- if prev_value != value
115
- @modified = true
116
- end
117
109
  end
118
110
  end
119
111
 
120
112
  class ContentSecurityPolicyConfigError < StandardError; end
121
113
  class ContentSecurityPolicyConfig
122
- CONFIG_KEY = :csp
123
114
  HEADER_NAME = "Content-Security-Policy".freeze
124
115
 
125
116
  ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
@@ -149,7 +140,6 @@ module SecureHeaders
149
140
  end
150
141
 
151
142
  class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig
152
- CONFIG_KEY = :csp_report_only
153
143
  HEADER_NAME = "Content-Security-Policy-Report-Only".freeze
154
144
 
155
145
  def report_only?
@@ -4,7 +4,6 @@ module SecureHeaders
4
4
 
5
5
  class ExpectCertificateTransparency
6
6
  HEADER_NAME = "Expect-CT".freeze
7
- CONFIG_KEY = :expect_certificate_transparency
8
7
  INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze
9
8
  INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze
10
9
  REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze
@@ -15,8 +14,8 @@ module SecureHeaders
15
14
  #
16
15
  # Returns nil if not configured, returns header name and value if
17
16
  # configured.
18
- def make_header(config)
19
- return if config.nil?
17
+ def make_header(config, use_agent = nil)
18
+ return if config.nil? || config == OPT_OUT
20
19
 
21
20
  header = new(config)
22
21
  [HEADER_NAME, header.value]
@@ -5,7 +5,6 @@ module SecureHeaders
5
5
  base.extend(ClassMethods)
6
6
  end
7
7
 
8
- MODERN_BROWSERS = %w(Chrome Opera Firefox)
9
8
  DEFAULT_CONFIG = {
10
9
  default_src: %w(https:),
11
10
  img_src: %w(https: data: 'self'),
@@ -200,7 +199,8 @@ module SecureHeaders
200
199
  #
201
200
  # Returns a default policy if no configuration is provided, or a
202
201
  # header name and value based on the config.
203
- def make_header(config, user_agent)
202
+ def make_header(config, user_agent = nil)
203
+ return if config.nil? || config == OPT_OUT
204
204
  header = new(config, user_agent)
205
205
  [header.name, header.value]
206
206
  end
@@ -215,27 +215,28 @@ module SecureHeaders
215
215
  if config.directive_value(:script_src).nil?
216
216
  raise ContentSecurityPolicyConfigError.new(":script_src is required, falling back to default-src is too dangerous. Use `script_src: OPT_OUT` to override")
217
217
  end
218
+ if !config.report_only? && config.directive_value(:report_only)
219
+ raise ContentSecurityPolicyConfigError.new("Only the csp_report_only config should set :report_only to true")
220
+ end
221
+
222
+ if config.report_only? && config.directive_value(:report_only) == false
223
+ raise ContentSecurityPolicyConfigError.new("csp_report_only config must have :report_only set to true")
224
+ end
218
225
 
219
226
  ContentSecurityPolicyConfig.attrs.each do |key|
220
227
  value = config.directive_value(key)
221
228
  next unless value
229
+
222
230
  if META_CONFIGS.include?(key)
223
231
  raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
232
+ elsif NONCES.include?(key)
233
+ raise ContentSecurityPolicyConfigError.new("#{key} must be a non-nil value") if value.nil?
224
234
  else
225
235
  validate_directive!(key, value)
226
236
  end
227
237
  end
228
238
  end
229
239
 
230
- # Public: check if a user agent supports CSP nonces
231
- #
232
- # user_agent - a String or a UserAgent object
233
- def nonces_supported?(user_agent)
234
- user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String)
235
- MODERN_BROWSERS.include?(user_agent.browser) ||
236
- user_agent.browser == "Safari" && (user_agent.version || CSP::FALLBACK_VERSION) >= CSP::VERSION_10
237
- end
238
-
239
240
  # Public: combine the values from two different configs.
240
241
  #
241
242
  # original - the main config
@@ -5,15 +5,14 @@ module SecureHeaders
5
5
  HEADER_NAME = "Public-Key-Pins".freeze
6
6
  REPORT_ONLY = "Public-Key-Pins-Report-Only".freeze
7
7
  HASH_ALGORITHMS = [:sha256].freeze
8
- CONFIG_KEY = :hpkp
9
8
 
10
9
 
11
10
  class << self
12
11
  # Public: make an hpkp header name, value pair
13
12
  #
14
13
  # Returns nil if not configured, returns header name and value if configured.
15
- def make_header(config)
16
- return if config.nil?
14
+ def make_header(config, user_agent = nil)
15
+ return if config.nil? || config == OPT_OUT
17
16
  header = new(config)
18
17
  [header.name, header.value]
19
18
  end
@@ -14,14 +14,14 @@ module SecureHeaders
14
14
  origin-when-cross-origin
15
15
  unsafe-url
16
16
  )
17
- CONFIG_KEY = :referrer_policy
18
17
 
19
18
  class << self
20
19
  # Public: generate an Referrer Policy header.
21
20
  #
22
21
  # Returns a default header if no configuration is provided, or a
23
22
  # header name and value based on the config.
24
- def make_header(config = nil)
23
+ def make_header(config = nil, user_agent = nil)
24
+ return if config == OPT_OUT
25
25
  config ||= DEFAULT_VALUE
26
26
  [HEADER_NAME, Array(config).join(", ")]
27
27
  end
@@ -8,14 +8,14 @@ module SecureHeaders
8
8
  DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE
9
9
  VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i
10
10
  MESSAGE = "The config value supplied for the HSTS header was invalid. Must match #{VALID_STS_HEADER}"
11
- CONFIG_KEY = :hsts
12
11
 
13
12
  class << self
14
13
  # Public: generate an hsts header name, value pair.
15
14
  #
16
15
  # Returns a default header if no configuration is provided, or a
17
16
  # header name and value based on the config.
18
- def make_header(config = nil)
17
+ def make_header(config = nil, user_agent = nil)
18
+ return if config == OPT_OUT
19
19
  [HEADER_NAME, config || DEFAULT_VALUE]
20
20
  end
21
21
 
@@ -5,14 +5,14 @@ module SecureHeaders
5
5
  class XContentTypeOptions
6
6
  HEADER_NAME = "X-Content-Type-Options".freeze
7
7
  DEFAULT_VALUE = "nosniff"
8
- CONFIG_KEY = :x_content_type_options
9
8
 
10
9
  class << self
11
10
  # Public: generate an X-Content-Type-Options header.
12
11
  #
13
12
  # Returns a default header if no configuration is provided, or a
14
13
  # header name and value based on the config.
15
- def make_header(config = nil)
14
+ def make_header(config = nil, user_agent = nil)
15
+ return if config == OPT_OUT
16
16
  [HEADER_NAME, config || DEFAULT_VALUE]
17
17
  end
18
18
 
@@ -4,14 +4,14 @@ module SecureHeaders
4
4
  class XDownloadOptions
5
5
  HEADER_NAME = "X-Download-Options".freeze
6
6
  DEFAULT_VALUE = "noopen"
7
- CONFIG_KEY = :x_download_options
8
7
 
9
8
  class << self
10
9
  # Public: generate an X-Download-Options header.
11
10
  #
12
11
  # Returns a default header if no configuration is provided, or a
13
12
  # header name and value based on the config.
14
- def make_header(config = nil)
13
+ def make_header(config = nil, user_agent = nil)
14
+ return if config == OPT_OUT
15
15
  [HEADER_NAME, config || DEFAULT_VALUE]
16
16
  end
17
17
 
@@ -3,7 +3,6 @@ module SecureHeaders
3
3
  class XFOConfigError < StandardError; end
4
4
  class XFrameOptions
5
5
  HEADER_NAME = "X-Frame-Options".freeze
6
- CONFIG_KEY = :x_frame_options
7
6
  SAMEORIGIN = "sameorigin"
8
7
  DENY = "deny"
9
8
  ALLOW_FROM = "allow-from"
@@ -16,7 +15,7 @@ module SecureHeaders
16
15
  #
17
16
  # Returns a default header if no configuration is provided, or a
18
17
  # header name and value based on the config.
19
- def make_header(config = nil)
18
+ def make_header(config = nil, user_agent = nil)
20
19
  return if config == OPT_OUT
21
20
  [HEADER_NAME, config || DEFAULT_VALUE]
22
21
  end
@@ -5,14 +5,14 @@ module SecureHeaders
5
5
  HEADER_NAME = "X-Permitted-Cross-Domain-Policies".freeze
6
6
  DEFAULT_VALUE = "none"
7
7
  VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename)
8
- CONFIG_KEY = :x_permitted_cross_domain_policies
9
8
 
10
9
  class << self
11
10
  # Public: generate an X-Permitted-Cross-Domain-Policies header.
12
11
  #
13
12
  # Returns a default header if no configuration is provided, or a
14
13
  # header name and value based on the config.
15
- def make_header(config = nil)
14
+ def make_header(config = nil, user_agent = nil)
15
+ return if config == OPT_OUT
16
16
  [HEADER_NAME, config || DEFAULT_VALUE]
17
17
  end
18
18
 
@@ -4,15 +4,15 @@ module SecureHeaders
4
4
  class XXssProtection
5
5
  HEADER_NAME = "X-XSS-Protection".freeze
6
6
  DEFAULT_VALUE = "1; mode=block"
7
- VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i
8
- CONFIG_KEY = :x_xss_protection
7
+ VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/
9
8
 
10
9
  class << self
11
10
  # Public: generate an X-Xss-Protection header.
12
11
  #
13
12
  # Returns a default header if no configuration is provided, or a
14
13
  # header name and value based on the config.
15
- def make_header(config = nil)
14
+ def make_header(config = nil, user_agent = nil)
15
+ return if config == OPT_OUT
16
16
  [HEADER_NAME, config || DEFAULT_VALUE]
17
17
  end
18
18
 
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
  Gem::Specification.new do |gem|
4
4
  gem.name = "secure_headers"
5
- gem.version = "5.2.0"
5
+ gem.version = "6.0.0.alpha01"
6
6
  gem.authors = ["Neil Matatall"]
7
7
  gem.email = ["neil.matatall@gmail.com"]
8
8
  gem.description = "Manages application of security headers with many safe defaults."
@@ -5,88 +5,33 @@ module SecureHeaders
5
5
  describe Configuration do
6
6
  before(:each) do
7
7
  reset_config
8
- Configuration.default
9
8
  end
10
9
 
11
10
  it "has a default config" do
12
- expect(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)).to_not be_nil
13
- end
14
-
15
- it "warns when using deprecated internal-ish #get API" do
16
- expect(Kernel).to receive(:warn).once.with(/`#get` is deprecated/)
17
- Configuration.get(Configuration::DEFAULT_CONFIG)
18
- end
19
-
20
- it "has an 'noop' config" do
21
- expect(Configuration.get(Configuration::NOOP_CONFIGURATION, internal: true)).to_not be_nil
22
- end
23
-
24
- it "precomputes headers upon creation" do
25
- default_config = Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)
26
- header_hash = default_config.cached_headers.each_with_object({}) do |(key, value), hash|
27
- header_name, header_value = if key == :csp
28
- value["Chrome"]
29
- else
30
- value
31
- end
32
-
33
- hash[header_name] = header_value
34
- end
35
- expect_default_values(header_hash)
11
+ expect(Configuration.default).to_not be_nil
36
12
  end
37
13
 
38
- it "copies config values when duping" do
39
- Configuration.override(:test_override, Configuration::NOOP_CONFIGURATION) do
40
- # do nothing, just copy it
41
- end
42
-
43
- config = Configuration.get(:test_override, internal: true)
44
- noop = Configuration.get(Configuration::NOOP_CONFIGURATION, internal: true)
45
- [:csp, :csp_report_only, :cookies].each do |key|
46
- expect(config.send(key)).to eq(noop.send(key))
47
- end
14
+ it "has an 'noop' override" do
15
+ Configuration.default
16
+ expect(Configuration.overrides(Configuration::NOOP_OVERRIDE)).to_not be_nil
48
17
  end
49
18
 
50
- it "regenerates cached headers when building an override" do
51
- Configuration.override(:test_override) do |config|
52
- config.x_content_type_options = OPT_OUT
19
+ it "dup results in a copy of the default config" do
20
+ Configuration.default
21
+ original_configuration = Configuration.send(:default_config)
22
+ configuration = Configuration.dup
23
+ expect(original_configuration).not_to be(configuration)
24
+ Configuration::CONFIG_ATTRIBUTES.each do |attr|
25
+ expect(original_configuration.send(attr)).to eq(configuration.send(attr))
53
26
  end
54
-
55
- expect(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).cached_headers).to_not eq(Configuration.get(:test_override, internal: true).cached_headers)
56
27
  end
57
28
 
58
- it "stores an override of the global config" do
29
+ it "stores an override" do
59
30
  Configuration.override(:test_override) do |config|
60
31
  config.x_frame_options = "DENY"
61
32
  end
62
33
 
63
- expect(Configuration.get(:test_override, internal: true)).to_not be_nil
64
- end
65
-
66
- it "deep dup's config values when overriding so the original cannot be modified" do
67
- Configuration.override(:override) do |config|
68
- config.csp[:default_src] << "'self'"
69
- end
70
-
71
- default = Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)
72
- override = Configuration.get(:override, internal: true)
73
-
74
- expect(override.csp.directive_value(:default_src)).not_to be(default.csp.directive_value(:default_src))
75
- end
76
-
77
- it "allows you to override an override" do
78
- Configuration.override(:override) do |config|
79
- config.csp = { default_src: %w('self'), script_src: %w('self')}
80
- end
81
-
82
- Configuration.override(:second_override, :override) do |config|
83
- config.csp = config.csp.merge(script_src: %w(example.org))
84
- end
85
-
86
- original_override = Configuration.get(:override, internal: true)
87
- expect(original_override.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self'))
88
- override_config = Configuration.get(:second_override, internal: true)
89
- expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org))
34
+ expect(Configuration.overrides(:test_override)).to_not be_nil
90
35
  end
91
36
 
92
37
  it "deprecates the secure_cookies configuration" do
@@ -106,7 +51,7 @@ module SecureHeaders
106
51
  config.cookies = OPT_OUT
107
52
  end
108
53
 
109
- config = Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)
54
+ config = Configuration.dup
110
55
  expect(config.cookies).to eq(OPT_OUT)
111
56
  end
112
57
 
@@ -115,7 +60,7 @@ module SecureHeaders
115
60
  config.cookies = {httponly: true, secure: true, samesite: {lax: false}}
116
61
  end
117
62
 
118
- config = Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)
63
+ config = Configuration.dup
119
64
  expect(config.cookies).to eq({httponly: true, secure: true, samesite: {lax: false}})
120
65
  end
121
66
  end