secure_headers 5.0.5 → 6.0.0.alpha01

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
 
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require "secure_headers/configuration"
3
2
  require "secure_headers/hash_helper"
4
3
  require "secure_headers/headers/cookie"
5
4
  require "secure_headers/headers/public_key_pins"
@@ -18,6 +17,7 @@ require "secure_headers/railtie"
18
17
  require "secure_headers/view_helper"
19
18
  require "useragent"
20
19
  require "singleton"
20
+ require "secure_headers/configuration"
21
21
 
22
22
  # All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
23
23
  # or ":optout_of_protection" as a config value to disable a given header
@@ -52,29 +52,9 @@ module SecureHeaders
52
52
  HTTPS = "https".freeze
53
53
  CSP = ContentSecurityPolicy
54
54
 
55
- ALL_HEADER_CLASSES = [
56
- ExpectCertificateTransparency,
57
- ClearSiteData,
58
- ContentSecurityPolicyConfig,
59
- ContentSecurityPolicyReportOnlyConfig,
60
- StrictTransportSecurity,
61
- PublicKeyPins,
62
- ReferrerPolicy,
63
- XContentTypeOptions,
64
- XDownloadOptions,
65
- XFrameOptions,
66
- XPermittedCrossDomainPolicies,
67
- XXssProtection
68
- ].freeze
69
-
70
- ALL_HEADERS_BESIDES_CSP = (
71
- ALL_HEADER_CLASSES -
72
- [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig]
73
- ).freeze
74
-
75
55
  # Headers set on http requests (excludes STS and HPKP)
76
- HTTP_HEADER_CLASSES =
77
- (ALL_HEADER_CLASSES - [StrictTransportSecurity, PublicKeyPins]).freeze
56
+ HTTPS_HEADER_CLASSES =
57
+ [StrictTransportSecurity, PublicKeyPins].freeze
78
58
 
79
59
  class << self
80
60
  # Public: override a given set of directives for the current request. If a
@@ -153,7 +133,7 @@ module SecureHeaders
153
133
  # Public: opts out of setting all headers by telling secure_headers to use
154
134
  # the NOOP configuration.
155
135
  def opt_out_of_all_protection(request)
156
- use_secure_headers_override(request, Configuration::NOOP_CONFIGURATION)
136
+ use_secure_headers_override(request, Configuration::NOOP_OVERRIDE)
157
137
  end
158
138
 
159
139
  # Public: Builds the hash of headers that should be applied base on the
@@ -168,27 +148,16 @@ module SecureHeaders
168
148
  def header_hash_for(request)
169
149
  prevent_dup = true
170
150
  config = config_for(request, prevent_dup)
171
- headers = config.cached_headers
151
+ config.validate_config!
172
152
  user_agent = UserAgent.parse(request.user_agent)
153
+ headers = config.generate_headers(user_agent)
173
154
 
174
- if !config.csp.opt_out? && config.csp.modified?
175
- headers = update_cached_csp(config.csp, headers, user_agent)
176
- end
177
-
178
- if !config.csp_report_only.opt_out? && config.csp_report_only.modified?
179
- headers = update_cached_csp(config.csp_report_only, headers, user_agent)
180
- end
181
-
182
- header_classes_for(request).each_with_object({}) do |klass, hash|
183
- if header = headers[klass::CONFIG_KEY]
184
- header_name, value = if klass == ContentSecurityPolicyConfig || klass == ContentSecurityPolicyReportOnlyConfig
185
- csp_header_for_ua(header, user_agent)
186
- else
187
- header
188
- end
189
- hash[header_name] = value
155
+ if request.scheme != HTTPS
156
+ HTTPS_HEADER_CLASSES.each do |klass|
157
+ headers.delete(klass::HEADER_NAME)
190
158
  end
191
159
  end
160
+ headers
192
161
  end
193
162
 
194
163
  # Public: specify which named override will be used for this request.
@@ -196,11 +165,9 @@ module SecureHeaders
196
165
  #
197
166
  # name - the name of the previously configured override.
198
167
  def use_secure_headers_override(request, name)
199
- if config = Configuration.get(name, internal: true)
200
- override_secure_headers_request_config(request, config)
201
- else
202
- raise ArgumentError.new("no override by the name of #{name} has been configured")
203
- end
168
+ config = config_for(request)
169
+ config.override(name)
170
+ override_secure_headers_request_config(request, config)
204
171
  end
205
172
 
206
173
  # Public: gets or creates a nonce for CSP.
@@ -228,7 +195,7 @@ module SecureHeaders
228
195
  # Falls back to the global config
229
196
  def config_for(request, prevent_dup = false)
230
197
  config = request.env[SECURE_HEADERS_CONFIG] ||
231
- Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)
198
+ Configuration.send(:default_config)
232
199
 
233
200
 
234
201
  # Global configs are frozen, per-request configs are not. When we're not
@@ -285,35 +252,6 @@ module SecureHeaders
285
252
  def override_secure_headers_request_config(request, config)
286
253
  request.env[SECURE_HEADERS_CONFIG] = config
287
254
  end
288
-
289
- # Private: determines which headers are applicable to a given request.
290
- #
291
- # Returns a list of classes whose corresponding header values are valid for
292
- # this request.
293
- def header_classes_for(request)
294
- if request.scheme == HTTPS
295
- ALL_HEADER_CLASSES
296
- else
297
- HTTP_HEADER_CLASSES
298
- end
299
- end
300
-
301
- def update_cached_csp(config, headers, user_agent)
302
- headers = Configuration.send(:deep_copy, headers)
303
- headers[config.class::CONFIG_KEY] = {}
304
- variation = ContentSecurityPolicy.ua_to_variation(user_agent)
305
- headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent)
306
- headers
307
- end
308
-
309
- # Private: chooses the applicable CSP header for the provided user agent.
310
- #
311
- # headers - a hash of header_config_key => [header_name, header_value]
312
- #
313
- # Returns a CSP [header, value] array
314
- def csp_header_for_ua(headers, user_agent)
315
- headers[ContentSecurityPolicy.ua_to_variation(user_agent)]
316
- end
317
255
  end
318
256
 
319
257
  # These methods are mixed into controllers and delegate to the class method
@@ -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.0.5"
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
@@ -124,7 +124,7 @@ module SecureHeaders
124
124
 
125
125
  it "supports strict-dynamic" do
126
126
  csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}, USER_AGENTS[:chrome])
127
- expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456'")
127
+ expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'")
128
128
  end
129
129
 
130
130
  context "browser sniffing" do
@@ -143,44 +143,44 @@ module SecureHeaders
143
143
 
144
144
  it "does not filter any directives for Chrome" do
145
145
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
146
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
146
+ expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
147
147
  end
148
148
 
149
149
  it "does not filter any directives for Opera" do
150
150
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
151
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
151
+ expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
152
152
  end
153
153
 
154
154
  it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
155
155
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
156
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
156
+ expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
157
157
  end
158
158
 
159
159
  it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
160
160
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
161
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
161
+ expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
162
162
  end
163
163
 
164
- it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for Edge" do
164
+ it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, hash sources, and plugin-types for Edge" do
165
165
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
166
- expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
166
+ expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
167
167
  end
168
168
 
169
- it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do
169
+ it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, hash sources, and plugin-types for safari" do
170
170
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
171
- expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
171
+ expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
172
172
  end
173
173
 
174
- it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do
174
+ it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, and hash sources for safari 10 and higher" do
175
175
  policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10])
176
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; report-uri report-uri.com")
176
+ expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
177
177
  end
178
178
 
179
179
  it "falls back to standard Firefox defaults when the useragent version is not present" do
180
180
  ua = USER_AGENTS[:firefox].dup
181
181
  allow(ua).to receive(:version).and_return(nil)
182
182
  policy = ContentSecurityPolicy.new(complex_opts, ua)
183
- expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
183
+ expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
184
184
  end
185
185
  end
186
186
  end
@@ -3,6 +3,11 @@ require "spec_helper"
3
3
 
4
4
  module SecureHeaders
5
5
  describe PolicyManagement do
6
+ before(:each) do
7
+ reset_config
8
+ Configuration.default
9
+ end
10
+
6
11
  let (:default_opts) do
7
12
  {
8
13
  default_src: %w(https:),
@@ -18,7 +23,7 @@ module SecureHeaders
18
23
  # (pulled from README)
19
24
  config = {
20
25
  # "meta" values. these will shape the header, but the values are not included in the header.
21
- report_only: true, # default: false
26
+ report_only: false,
22
27
  preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
23
28
 
24
29
  # directive values: these values will directly translate into source directives
@@ -142,9 +147,24 @@ module SecureHeaders
142
147
  ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["application/pdf"])))
143
148
  end.to_not raise_error
144
149
  end
150
+
151
+ it "doesn't allow report_only to be set in a non-report-only config" do
152
+ expect do
153
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: true)))
154
+ end.to raise_error(ContentSecurityPolicyConfigError)
155
+ end
156
+
157
+ it "allows report_only to be set in a report-only config" do
158
+ expect do
159
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyReportOnlyConfig.new(default_opts.merge(report_only: true)))
160
+ end.to_not raise_error
161
+ end
145
162
  end
146
163
 
147
164
  describe "#combine_policies" do
165
+ before(:each) do
166
+ reset_config
167
+ end
148
168
  it "combines the default-src value with the override if the directive was unconfigured" do
149
169
  Configuration.default do |config|
150
170
  config.csp = {
@@ -152,7 +172,8 @@ module SecureHeaders
152
172
  script_src: %w('self'),
153
173
  }
154
174
  end
155
- combined_config = ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, style_src: %w(anothercdn.com))
175
+ default_policy = Configuration.dup
176
+ combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, style_src: %w(anothercdn.com))
156
177
  csp = ContentSecurityPolicy.new(combined_config)
157
178
  expect(csp.name).to eq(ContentSecurityPolicyConfig::HEADER_NAME)
158
179
  expect(csp.value).to eq("default-src https:; script-src 'self'; style-src https: anothercdn.com")
@@ -167,7 +188,8 @@ module SecureHeaders
167
188
  }.freeze
168
189
  end
169
190
  report_uri = "https://report-uri.io/asdf"
170
- combined_config = ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, report_uri: [report_uri])
191
+ default_policy = Configuration.dup
192
+ combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_uri: [report_uri])
171
193
  csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
172
194
  expect(csp.value).to include("report-uri #{report_uri}")
173
195
  end
@@ -183,7 +205,8 @@ module SecureHeaders
183
205
  non_default_source_additions = ContentSecurityPolicy::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash|
184
206
  hash[directive] = %w("http://example.org)
185
207
  end
186
- combined_config = ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, non_default_source_additions)
208
+ default_policy = Configuration.dup
209
+ combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, non_default_source_additions)
187
210
 
188
211
  ContentSecurityPolicy::NON_FETCH_SOURCES.each do |directive|
189
212
  expect(combined_config[directive]).to eq(%w("http://example.org))
@@ -198,7 +221,8 @@ module SecureHeaders
198
221
  report_only: false
199
222
  }
200
223
  end
201
- combined_config = ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, report_only: true)
224
+ default_policy = Configuration.dup
225
+ combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_only: true)
202
226
  csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
203
227
  expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME)
204
228
  end
@@ -211,7 +235,8 @@ module SecureHeaders
211
235
  block_all_mixed_content: false
212
236
  }
213
237
  end
214
- combined_config = ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, block_all_mixed_content: true)
238
+ default_policy = Configuration.dup
239
+ combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, block_all_mixed_content: true)
215
240
  csp = ContentSecurityPolicy.new(combined_config)
216
241
  expect(csp.value).to eq("default-src https:; block-all-mixed-content; script-src 'self'")
217
242
  end
@@ -220,8 +245,9 @@ module SecureHeaders
220
245
  Configuration.default do |config|
221
246
  config.csp = OPT_OUT
222
247
  end
248
+ default_policy = Configuration.dup
223
249
  expect do
224
- ContentSecurityPolicy.combine_policies(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).csp.to_h, script_src: %w(anothercdn.com))
250
+ ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, script_src: %w(anothercdn.com))
225
251
  end.to raise_error(ContentSecurityPolicyConfigError)
226
252
  end
227
253
  end
@@ -16,6 +16,7 @@ module SecureHeaders
16
16
 
17
17
  it "warns if the hpkp report-uri host is the same as the current host" do
18
18
  report_host = "report-uri.io"
19
+ reset_config
19
20
  Configuration.default do |config|
20
21
  config.hpkp = {
21
22
  max_age: 10000000,
@@ -50,12 +51,14 @@ module SecureHeaders
50
51
  end
51
52
  request = Rack::Request.new({})
52
53
  SecureHeaders.use_secure_headers_override(request, "my_custom_config")
53
- expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config", internal: true))
54
54
  _, env = middleware.call request.env
55
55
  expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match("example.org")
56
56
  end
57
57
 
58
58
  context "cookies" do
59
+ before(:each) do
60
+ reset_config
61
+ end
59
62
  context "cookies should be flagged" do
60
63
  it "flags cookies as secure" do
61
64
  Configuration.default { |config| config.cookies = {secure: true, httponly: OPT_OUT, samesite: OPT_OUT} }
@@ -87,6 +90,9 @@ module SecureHeaders
87
90
  end
88
91
 
89
92
  context "cookies" do
93
+ before(:each) do
94
+ reset_config
95
+ end
90
96
  it "flags cookies from configuration" do
91
97
  Configuration.default { |config| config.cookies = { secure: true, httponly: true, samesite: { lax: true} } }
92
98
  request = Rack::Request.new("HTTPS" => "on")
@@ -105,6 +105,7 @@ module SecureHeaders
105
105
  let(:filename) { "app/views/asdfs/index.html.erb" }
106
106
 
107
107
  before(:all) do
108
+ reset_config
108
109
  Configuration.default do |config|
109
110
  config.csp = {
110
111
  default_src: %w('self'),