secure_headers 3.4.1 → 3.5.0

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.
@@ -7,9 +7,6 @@ module SecureHeaders
7
7
  MODERN_BROWSERS = %w(Chrome Opera Firefox)
8
8
  DEFAULT_VALUE = "default-src https:".freeze
9
9
  DEFAULT_CONFIG = { default_src: %w(https:) }.freeze
10
- HEADER_NAME = "Content-Security-Policy".freeze
11
- REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
12
- HEADER_NAMES = [HEADER_NAME, REPORT_ONLY]
13
10
  DATA_PROTOCOL = "data:".freeze
14
11
  BLOB_PROTOCOL = "blob:".freeze
15
12
  SELF = "'self'".freeze
@@ -158,13 +155,13 @@ module SecureHeaders
158
155
  PLUGIN_TYPES => :source_list,
159
156
  REFLECTED_XSS => :string,
160
157
  REPORT_URI => :source_list,
161
- SANDBOX => :string,
158
+ SANDBOX => :source_list,
162
159
  SCRIPT_SRC => :source_list,
163
160
  STYLE_SRC => :source_list,
164
161
  UPGRADE_INSECURE_REQUESTS => :boolean
165
162
  }.freeze
166
163
 
167
- CONFIG_KEY = :csp
164
+
168
165
  STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
169
166
  HTTP_SCHEME_REGEX = %r{\Ahttps?://}
170
167
 
@@ -181,6 +178,11 @@ module SecureHeaders
181
178
  :preserve_schemes
182
179
  ].freeze
183
180
 
181
+ NONCES = [
182
+ :script_nonce,
183
+ :style_nonce
184
+ ].freeze
185
+
184
186
  module ClassMethods
185
187
  # Public: generate a header name, value array that is user-agent-aware.
186
188
  #
@@ -196,9 +198,11 @@ module SecureHeaders
196
198
  # Does not validate the invididual values of the source expression (e.g.
197
199
  # script_src => h*t*t*p: will not raise an exception)
198
200
  def validate_config!(config)
199
- return if config.nil? || config == OPT_OUT
200
- raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src]
201
- config.each do |key, value|
201
+ return if config.nil? || config.opt_out?
202
+ raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src)
203
+ ContentSecurityPolicyConfig.attrs.each do |key|
204
+ value = config.directive_value(key)
205
+ next unless value
202
206
  if META_CONFIGS.include?(key)
203
207
  raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
204
208
  else
@@ -207,16 +211,13 @@ module SecureHeaders
207
211
  end
208
212
  end
209
213
 
210
- # Public: determine if merging +additions+ will cause a change to the
211
- # actual value of the config.
214
+ # Public: check if a user agent supports CSP nonces
212
215
  #
213
- # e.g. config = { script_src: %w(example.org google.com)} and
214
- # additions = { script_src: %w(google.com)} then idempotent_additions? would return
215
- # because google.com is already in the config.
216
- def idempotent_additions?(config, additions)
217
- return true if config == OPT_OUT && additions == OPT_OUT
218
- return false if config == OPT_OUT
219
- config == combine_policies(config, additions)
216
+ # user_agent - a String or a UserAgent object
217
+ def nonces_supported?(user_agent)
218
+ user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String)
219
+ MODERN_BROWSERS.include?(user_agent.browser) ||
220
+ user_agent.browser == "Safari" && user_agent.version >= CSP::VERSION_10
220
221
  end
221
222
 
222
223
  # Public: combine the values from two different configs.
@@ -233,7 +234,7 @@ module SecureHeaders
233
234
  # 3. if a value in additions does exist in the original config, the two
234
235
  # values are joined.
235
236
  def combine_policies(original, additions)
236
- if original == OPT_OUT
237
+ if original == {}
237
238
  raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
238
239
  end
239
240
 
@@ -34,6 +34,14 @@ module SecureHeaders
34
34
  end
35
35
  end
36
36
 
37
+ def content_security_policy_script_nonce
38
+ content_security_policy_nonce(:script)
39
+ end
40
+
41
+ def content_security_policy_style_nonce
42
+ content_security_policy_nonce(:style)
43
+ end
44
+
37
45
  ##
38
46
  # Checks to see if the hashed code is expected and adds the hash source
39
47
  # value to the current CSP.
@@ -14,18 +14,44 @@ require "secure_headers/middleware"
14
14
  require "secure_headers/railtie"
15
15
  require "secure_headers/view_helper"
16
16
  require "useragent"
17
+ require "singleton"
17
18
 
18
19
  # All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
19
20
  # or ":optout_of_protection" as a config value to disable a given header
20
21
  module SecureHeaders
21
- OPT_OUT = :opt_out_of_protection
22
+ class NoOpHeaderConfig
23
+ include Singleton
24
+
25
+ def boom(arg = nil)
26
+ raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead."
27
+ end
28
+
29
+ def to_h
30
+ {}
31
+ end
32
+
33
+ def dup
34
+ self.class.instance
35
+ end
36
+
37
+ def opt_out?
38
+ true
39
+ end
40
+
41
+ alias_method :[], :boom
42
+ alias_method :[]=, :boom
43
+ alias_method :keys, :boom
44
+ end
45
+
46
+ OPT_OUT = NoOpHeaderConfig.instance
22
47
  SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze
23
48
  NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze
24
49
  HTTPS = "https".freeze
25
50
  CSP = ContentSecurityPolicy
26
51
 
27
52
  ALL_HEADER_CLASSES = [
28
- ContentSecurityPolicy,
53
+ ContentSecurityPolicyConfig,
54
+ ContentSecurityPolicyReportOnlyConfig,
29
55
  StrictTransportSecurity,
30
56
  PublicKeyPins,
31
57
  ReferrerPolicy,
@@ -36,7 +62,10 @@ module SecureHeaders
36
62
  XXssProtection
37
63
  ].freeze
38
64
 
39
- ALL_HEADERS_BESIDES_CSP = (ALL_HEADER_CLASSES - [CSP]).freeze
65
+ ALL_HEADERS_BESIDES_CSP = (
66
+ ALL_HEADER_CLASSES -
67
+ [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig]
68
+ ).freeze
40
69
 
41
70
  # Headers set on http requests (excludes STS and HPKP)
42
71
  HTTP_HEADER_CLASSES =
@@ -50,13 +79,25 @@ module SecureHeaders
50
79
  #
51
80
  # additions - a hash containing directives. e.g.
52
81
  # script_src: %w(another-host.com)
53
- def override_content_security_policy_directives(request, additions)
54
- config = config_for(request)
55
- if config.current_csp == OPT_OUT
56
- config.dynamic_csp = {}
82
+ def override_content_security_policy_directives(request, additions, target = nil)
83
+ config, target = config_and_target(request, target)
84
+
85
+ if [:both, :enforced].include?(target)
86
+ if config.csp.opt_out?
87
+ config.csp = ContentSecurityPolicyConfig.new({})
88
+ end
89
+
90
+ config.csp.merge!(additions)
91
+ end
92
+
93
+ if [:both, :report_only].include?(target)
94
+ if config.csp_report_only.opt_out?
95
+ config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({})
96
+ end
97
+
98
+ config.csp_report_only.merge!(additions)
57
99
  end
58
100
 
59
- config.dynamic_csp = config.current_csp.merge(additions)
60
101
  override_secure_headers_request_config(request, config)
61
102
  end
62
103
 
@@ -66,9 +107,17 @@ module SecureHeaders
66
107
  #
67
108
  # additions - a hash containing directives. e.g.
68
109
  # script_src: %w(another-host.com)
69
- def append_content_security_policy_directives(request, additions)
70
- config = config_for(request)
71
- config.dynamic_csp = CSP.combine_policies(config.current_csp, additions)
110
+ def append_content_security_policy_directives(request, additions, target = nil)
111
+ config, target = config_and_target(request, target)
112
+
113
+ if [:both, :enforced].include?(target) && !config.csp.opt_out?
114
+ config.csp.append(additions)
115
+ end
116
+
117
+ if [:both, :report_only].include?(target) && !config.csp_report_only.opt_out?
118
+ config.csp_report_only.append(additions)
119
+ end
120
+
72
121
  override_secure_headers_request_config(request, config)
73
122
  end
74
123
 
@@ -112,12 +161,28 @@ module SecureHeaders
112
161
  # returned is meant to be merged into the header value from `@app.call(env)`
113
162
  # in Rack middleware.
114
163
  def header_hash_for(request)
115
- config = config_for(request)
116
- unless ContentSecurityPolicy.idempotent_additions?(config.csp, config.current_csp)
117
- config.rebuild_csp_header_cache!(request.user_agent)
164
+ config = config_for(request, prevent_dup = true)
165
+ headers = config.cached_headers
166
+ user_agent = UserAgent.parse(request.user_agent)
167
+
168
+ if !config.csp.opt_out? && config.csp.modified?
169
+ headers = update_cached_csp(config.csp, headers, user_agent)
118
170
  end
119
171
 
120
- use_cached_headers(config.cached_headers, request)
172
+ if !config.csp_report_only.opt_out? && config.csp_report_only.modified?
173
+ headers = update_cached_csp(config.csp_report_only, headers, user_agent)
174
+ end
175
+
176
+ header_classes_for(request).each_with_object({}) do |klass, hash|
177
+ if header = headers[klass::CONFIG_KEY]
178
+ header_name, value = if [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig].include?(klass)
179
+ csp_header_for_ua(header, user_agent)
180
+ else
181
+ header
182
+ end
183
+ hash[header_name] = value
184
+ end
185
+ end
121
186
  end
122
187
 
123
188
  # Public: specify which named override will be used for this request.
@@ -138,7 +203,7 @@ module SecureHeaders
138
203
  #
139
204
  # Returns the nonce
140
205
  def content_security_policy_script_nonce(request)
141
- content_security_policy_nonce(request, CSP::SCRIPT_SRC)
206
+ content_security_policy_nonce(request, ContentSecurityPolicy::SCRIPT_SRC)
142
207
  end
143
208
 
144
209
  # Public: gets or creates a nonce for CSP.
@@ -147,7 +212,7 @@ module SecureHeaders
147
212
  #
148
213
  # Returns the nonce
149
214
  def content_security_policy_style_nonce(request)
150
- content_security_policy_nonce(request, CSP::STYLE_SRC)
215
+ content_security_policy_nonce(request, ContentSecurityPolicy::STYLE_SRC)
151
216
  end
152
217
 
153
218
  # Public: Retreives the config for a given header type:
@@ -155,11 +220,15 @@ module SecureHeaders
155
220
  # Checks to see if there is an override for this request, then
156
221
  # Checks to see if a named override is used for this request, then
157
222
  # Falls back to the global config
158
- def config_for(request)
223
+ def config_for(request, prevent_dup = false)
159
224
  config = request.env[SECURE_HEADERS_CONFIG] ||
160
225
  Configuration.get(Configuration::DEFAULT_CONFIG)
161
226
 
162
- if config.frozen?
227
+
228
+ # Global configs are frozen, per-request configs are not. When we're not
229
+ # making modifications to the config, prevent_dup ensures we don't dup
230
+ # the object unnecessarily. It's not necessarily frozen to begin with.
231
+ if config.frozen? && !prevent_dup
163
232
  config.dup
164
233
  else
165
234
  config
@@ -167,13 +236,38 @@ module SecureHeaders
167
236
  end
168
237
 
169
238
  private
239
+ TARGETS = [:both, :enforced, :report_only]
240
+ def raise_on_unknown_target(target)
241
+ unless TARGETS.include?(target)
242
+ raise "Unrecognized target: #{target}. Must be [:both, :enforced, :report_only]"
243
+ end
244
+ end
245
+
246
+ def config_and_target(request, target)
247
+ config = config_for(request)
248
+ target = guess_target(config) unless target
249
+ raise_on_unknown_target(target)
250
+ [config, target]
251
+ end
252
+
253
+ def guess_target(config)
254
+ if !config.csp.opt_out? && !config.csp_report_only.opt_out?
255
+ :both
256
+ elsif !config.csp.opt_out?
257
+ :enforced
258
+ elsif !config.csp_report_only.opt_out?
259
+ :report_only
260
+ else
261
+ :both
262
+ end
263
+ end
170
264
 
171
265
  # Private: gets or creates a nonce for CSP.
172
266
  #
173
267
  # Returns the nonce
174
268
  def content_security_policy_nonce(request, script_or_style)
175
269
  request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp
176
- nonce_key = script_or_style == CSP::SCRIPT_SRC ? :script_nonce : :style_nonce
270
+ nonce_key = script_or_style == ContentSecurityPolicy::SCRIPT_SRC ? :script_nonce : :style_nonce
177
271
  append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY])
178
272
  request.env[NONCE_KEY]
179
273
  end
@@ -198,21 +292,12 @@ module SecureHeaders
198
292
  end
199
293
  end
200
294
 
201
- # Private: takes a precomputed hash of headers and returns the Headers
202
- # customized for the request.
203
- #
204
- # Returns a hash of header names / values valid for a given request.
205
- def use_cached_headers(headers, request)
206
- header_classes_for(request).each_with_object({}) do |klass, hash|
207
- if header = headers[klass::CONFIG_KEY]
208
- header_name, value = if klass == CSP
209
- csp_header_for_ua(header, request)
210
- else
211
- header
212
- end
213
- hash[header_name] = value
214
- end
215
- end
295
+ def update_cached_csp(config, headers, user_agent)
296
+ headers = Configuration.send(:deep_copy, headers)
297
+ headers[config.class::CONFIG_KEY] = {}
298
+ variation = ContentSecurityPolicy.ua_to_variation(user_agent)
299
+ headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent)
300
+ headers
216
301
  end
217
302
 
218
303
  # Private: chooses the applicable CSP header for the provided user agent.
@@ -220,26 +305,8 @@ module SecureHeaders
220
305
  # headers - a hash of header_config_key => [header_name, header_value]
221
306
  #
222
307
  # Returns a CSP [header, value] array
223
- def csp_header_for_ua(headers, request)
224
- headers[CSP.ua_to_variation(UserAgent.parse(request.user_agent))]
225
- end
226
-
227
- # Private: optionally build a header with a given configure
228
- #
229
- # klass - corresponding Class for a given header
230
- # config - A string, symbol, or hash config for the header
231
- # user_agent - A string representing the UA (only used for CSP feature sniffing)
232
- #
233
- # Returns a 2 element array [header_name, header_value] or nil if config
234
- # is OPT_OUT
235
- def make_header(klass, header_config, user_agent = nil)
236
- unless header_config == OPT_OUT
237
- if klass == CSP
238
- klass.make_header(header_config, user_agent)
239
- else
240
- klass.make_header(header_config)
241
- end
242
- end
308
+ def csp_header_for_ua(headers, user_agent)
309
+ headers[ContentSecurityPolicy.ua_to_variation(user_agent)]
243
310
  end
244
311
  end
245
312
 
@@ -1,7 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  Gem::Specification.new do |gem|
3
3
  gem.name = "secure_headers"
4
- gem.version = "3.4.1"
4
+ gem.version = "3.5.0"
5
5
  gem.authors = ["Neil Matatall"]
6
6
  gem.email = ["neil.matatall@gmail.com"]
7
7
  gem.description = 'Security related headers all in one gem.'
@@ -36,8 +36,8 @@ module SecureHeaders
36
36
 
37
37
  config = Configuration.get(:test_override)
38
38
  noop = Configuration.get(Configuration::NOOP_CONFIGURATION)
39
- [:csp, :dynamic_csp, :cookies].each do |key|
40
- expect(config.send(key)).to eq(noop.send(key)), "Value not copied: #{key}."
39
+ [:csp, :csp_report_only, :cookies].each do |key|
40
+ expect(config.send(key)).to eq(noop.send(key))
41
41
  end
42
42
  end
43
43
 
@@ -65,7 +65,7 @@ module SecureHeaders
65
65
  default = Configuration.get
66
66
  override = Configuration.get(:override)
67
67
 
68
- expect(override.csp).not_to eq(default.csp)
68
+ expect(override.csp.directive_value(:default_src)).not_to be(default.csp.directive_value(:default_src))
69
69
  end
70
70
 
71
71
  it "allows you to override an override" do
@@ -78,9 +78,9 @@ module SecureHeaders
78
78
  end
79
79
 
80
80
  original_override = Configuration.get(:override)
81
- expect(original_override.csp).to eq(default_src: %w('self'))
81
+ expect(original_override.csp.to_h).to eq(default_src: %w('self'))
82
82
  override_config = Configuration.get(:second_override)
83
- expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org))
83
+ expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org))
84
84
  end
85
85
 
86
86
  it "deprecates the secure_cookies configuration" do
@@ -14,11 +14,11 @@ module SecureHeaders
14
14
 
15
15
  describe "#name" do
16
16
  context "when in report-only mode" do
17
- specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(ContentSecurityPolicy::HEADER_NAME + "-Report-Only") }
17
+ specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME) }
18
18
  end
19
19
 
20
20
  context "when in enforce mode" do
21
- specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(ContentSecurityPolicy::HEADER_NAME) }
21
+ specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(ContentSecurityPolicyConfig::HEADER_NAME) }
22
22
  end
23
23
  end
24
24
 
@@ -40,70 +40,75 @@ module SecureHeaders
40
40
  report_uri: %w(https://example.com/uri-directive)
41
41
  }
42
42
 
43
- CSP.validate_config!(config)
43
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(config))
44
44
  end
45
45
 
46
46
  it "requires a :default_src value" do
47
47
  expect do
48
- CSP.validate_config!(script_src: %('self'))
48
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(script_src: %('self')))
49
49
  end.to raise_error(ContentSecurityPolicyConfigError)
50
50
  end
51
51
 
52
52
  it "requires :report_only to be a truthy value" do
53
53
  expect do
54
- CSP.validate_config!(default_opts.merge(report_only: "steve"))
54
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: "steve")))
55
55
  end.to raise_error(ContentSecurityPolicyConfigError)
56
56
  end
57
57
 
58
58
  it "requires :preserve_schemes to be a truthy value" do
59
59
  expect do
60
- CSP.validate_config!(default_opts.merge(preserve_schemes: "steve"))
60
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(preserve_schemes: "steve")))
61
61
  end.to raise_error(ContentSecurityPolicyConfigError)
62
62
  end
63
63
 
64
64
  it "requires :block_all_mixed_content to be a boolean value" do
65
65
  expect do
66
- CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve"))
66
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(block_all_mixed_content: "steve")))
67
67
  end.to raise_error(ContentSecurityPolicyConfigError)
68
68
  end
69
69
 
70
70
  it "requires :upgrade_insecure_requests to be a boolean value" do
71
71
  expect do
72
- CSP.validate_config!(default_opts.merge(upgrade_insecure_requests: "steve"))
72
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(upgrade_insecure_requests: "steve")))
73
73
  end.to raise_error(ContentSecurityPolicyConfigError)
74
74
  end
75
75
 
76
76
  it "requires all source lists to be an array of strings" do
77
77
  expect do
78
- CSP.validate_config!(default_src: "steve")
78
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: "steve"))
79
79
  end.to raise_error(ContentSecurityPolicyConfigError)
80
80
  end
81
81
 
82
82
  it "allows nil values" do
83
83
  expect do
84
- CSP.validate_config!(default_src: %w('self'), script_src: ["https:", nil])
84
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), script_src: ["https:", nil]))
85
85
  end.to_not raise_error
86
86
  end
87
87
 
88
88
  it "rejects unknown directives / config" do
89
89
  expect do
90
- CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve")
90
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), default_src_totally_mispelled: "steve"))
91
91
  end.to raise_error(ContentSecurityPolicyConfigError)
92
92
  end
93
93
 
94
94
  # this is mostly to ensure people don't use the antiquated shorthands common in other configs
95
95
  it "performs light validation on source lists" do
96
96
  expect do
97
- CSP.validate_config!(default_src: %w(self none inline eval))
97
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval)))
98
98
  end.to raise_error(ContentSecurityPolicyConfigError)
99
99
  end
100
100
  end
101
101
 
102
102
  describe "#combine_policies" do
103
103
  it "combines the default-src value with the override if the directive was unconfigured" do
104
- combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com))
104
+ Configuration.default do |config|
105
+ config.csp = {
106
+ default_src: %w(https:)
107
+ }
108
+ end
109
+ combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com))
105
110
  csp = ContentSecurityPolicy.new(combined_config)
106
- expect(csp.name).to eq(CSP::HEADER_NAME)
111
+ expect(csp.name).to eq(ContentSecurityPolicyConfig::HEADER_NAME)
107
112
  expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com")
108
113
  end
109
114
 
@@ -115,7 +120,7 @@ module SecureHeaders
115
120
  }.freeze
116
121
  end
117
122
  report_uri = "https://report-uri.io/asdf"
118
- combined_config = CSP.combine_policies(Configuration.get.csp, report_uri: [report_uri])
123
+ combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, report_uri: [report_uri])
119
124
  csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
120
125
  expect(csp.value).to include("report-uri #{report_uri}")
121
126
  end
@@ -127,12 +132,12 @@ module SecureHeaders
127
132
  report_only: false
128
133
  }.freeze
129
134
  end
130
- non_default_source_additions = CSP::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash|
135
+ non_default_source_additions = ContentSecurityPolicy::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash|
131
136
  hash[directive] = %w("http://example.org)
132
137
  end
133
- combined_config = CSP.combine_policies(Configuration.get.csp, non_default_source_additions)
138
+ combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, non_default_source_additions)
134
139
 
135
- CSP::NON_FETCH_SOURCES.each do |directive|
140
+ ContentSecurityPolicy::NON_FETCH_SOURCES.each do |directive|
136
141
  expect(combined_config[directive]).to eq(%w("http://example.org))
137
142
  end
138
143
 
@@ -146,9 +151,9 @@ module SecureHeaders
146
151
  report_only: false
147
152
  }
148
153
  end
149
- combined_config = CSP.combine_policies(Configuration.get.csp, report_only: true)
154
+ combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, report_only: true)
150
155
  csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
151
- expect(csp.name).to eq(CSP::REPORT_ONLY)
156
+ expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME)
152
157
  end
153
158
 
154
159
  it "overrides the :block_all_mixed_content flag" do
@@ -158,7 +163,7 @@ module SecureHeaders
158
163
  block_all_mixed_content: false
159
164
  }
160
165
  end
161
- combined_config = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true)
166
+ combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, block_all_mixed_content: true)
162
167
  csp = ContentSecurityPolicy.new(combined_config)
163
168
  expect(csp.value).to eq("default-src https:; block-all-mixed-content")
164
169
  end
@@ -168,23 +173,9 @@ module SecureHeaders
168
173
  config.csp = OPT_OUT
169
174
  end
170
175
  expect do
171
- CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com))
176
+ ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com))
172
177
  end.to raise_error(ContentSecurityPolicyConfigError)
173
178
  end
174
179
  end
175
-
176
- describe "#idempotent_additions?" do
177
- specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false }
178
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false }
179
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false }
180
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false }
181
-
182
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true }
183
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true }
184
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true }
185
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true }
186
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true }
187
- specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true }
188
- end
189
180
  end
190
181
  end
@@ -51,7 +51,7 @@ module SecureHeaders
51
51
  SecureHeaders.use_secure_headers_override(request, "my_custom_config")
52
52
  expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config"))
53
53
  _, env = middleware.call request.env
54
- expect(env[CSP::HEADER_NAME]).to match("example.org")
54
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match("example.org")
55
55
  end
56
56
 
57
57
  context "secure_cookies" do
@@ -105,6 +105,18 @@ module SecureHeaders
105
105
  _, env = cookie_middleware.call request.env
106
106
  expect(env['Set-Cookie']).to eq("foo=bar")
107
107
  end
108
+
109
+ it "sets the secure cookie flag correctly on interleaved http/https requests" do
110
+ Configuration.default { |config| config.cookies = { secure: true } }
111
+
112
+ request = Rack::Request.new("HTTPS" => "off")
113
+ _, env = cookie_middleware.call request.env
114
+ expect(env['Set-Cookie']).to eq("foo=bar")
115
+
116
+ request = Rack::Request.new("HTTPS" => "on")
117
+ _, env = cookie_middleware.call request.env
118
+ expect(env['Set-Cookie']).to eq("foo=bar; secure")
119
+ end
108
120
  end
109
121
  end
110
122
  end
@@ -27,6 +27,16 @@ class Message < ERB
27
27
  background-color: black;
28
28
  }
29
29
  <% end %>
30
+
31
+ <script nonce="<%= content_security_policy_script_nonce %>">
32
+ alert(1)
33
+ </script>
34
+
35
+ <style nonce="<%= content_security_policy_style_nonce %>">
36
+ body {
37
+ background-color: black;
38
+ }
39
+ </style>
30
40
  <%= @name %>
31
41
 
32
42
  TEMPLATE
@@ -72,8 +82,11 @@ module SecureHeaders
72
82
 
73
83
  before(:all) do
74
84
  Configuration.default do |config|
75
- config.csp[:script_src] = %w('self')
76
- config.csp[:style_src] = %w('self')
85
+ config.csp = {
86
+ :default_src => %w('self'),
87
+ :script_src => %w('self'),
88
+ :style_src => %w('self')
89
+ }
77
90
  end
78
91
  end
79
92
 
@@ -115,10 +128,10 @@ module SecureHeaders
115
128
  Message.new(request).result
116
129
  _, env = middleware.call request.env
117
130
 
118
- expect(env[CSP::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/)
119
- expect(env[CSP::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/)
120
- expect(env[CSP::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/)
121
- expect(env[CSP::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/)
131
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/)
132
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/)
133
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/)
134
+ expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/)
122
135
  end
123
136
  end
124
137
  end