secure_headers 3.4.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 88233251c7a26e1269f77957d56823619e3d9d11
4
- data.tar.gz: 8e9c0e69fcca0298ee52223d587134f5a8df062f
3
+ metadata.gz: f47871f10c97316165d3f4230ddae85935d9a736
4
+ data.tar.gz: f5777312e07643e054d48b4121ba30a905aab4dd
5
5
  SHA512:
6
- metadata.gz: f421d0d58eb9b5a7b22fd13932644c2c3868e0294886cbf5250127e8807fa20dec95717219e0a82ce59f49f376b32e17b97b77c7e011d2e2d1633162c1297bbc
7
- data.tar.gz: ae63cbdd588bf6c31e531bfec5480e022a5df96c8ee2cc4c824d1e869f64df2a44073d795cc6d5e7ffeca6d6369f0d2e33f202ce8ceda04d274849e95789ff43
6
+ metadata.gz: 8516799304bf21ea8d67aace99325df8c34d1e3d72082b44ad4a80c658ddc9cee6e4a04fc8202ac062044c0abbc91ce870fc26a472960f390fe359670f7362cf
7
+ data.tar.gz: d4fc7f5482fc8e904ce3ea80340da5c5426b87c88bbb8f669af9e050b212a1189b997de0c3c6bdc9a3b4df58c69efa54598ea40caaf56eb9bc69a80722029f8c
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.2.3
1
+ 2.2.5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,69 @@
1
+ ## 3.5.0
2
+
3
+ This release adds support for setting two CSP headers (enforced/report-only) and management around them.
4
+
5
+
6
+ ## 3.4.1 Named Appends
7
+
8
+ ### Small bugfix
9
+
10
+ If your CSP did not define a script/style-src and you tried to use a script/style nonce, the nonce would be added to the page but it would not be added to the CSP. A workaround is to define a script/style src but now it should add the missing directive (and populate it with the default-src).
11
+
12
+ ### Named Appends
13
+
14
+ Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility.
15
+
16
+ ```ruby
17
+ def show
18
+ if include_widget?
19
+ @widget = widget.render
20
+ use_content_security_policy_named_append(:widget_partial)
21
+ end
22
+ end
23
+
24
+
25
+ SecureHeaders::Configuration.named_append(:widget_partial) do |request|
26
+ if request.controller_instance.current_user.in_test_bucket?
27
+ SecureHeaders.override_x_frame_options(request, "DENY")
28
+ { child_src: %w(beta.thirdpartyhost.com) }
29
+ else
30
+ { child_src: %w(thirdpartyhost.com) }
31
+ end
32
+ end
33
+ ```
34
+
35
+ You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following:
36
+
37
+ ```ruby
38
+ SecureHeader::Configuration.default do |config|
39
+ config.csp = { default_src: %w('self')}
40
+ end
41
+
42
+ SecureHeaders::Configuration.named_append(:A) do |request|
43
+ { default_src: %w(myhost.com) }
44
+ end
45
+
46
+ SecureHeaders::Configuration.named_append(:B) do |request|
47
+ { script_src: %w('unsafe-eval') }
48
+ end
49
+ ```
50
+
51
+ The following code will produce different policies due to the way policies are normalized (e.g. providing a previously undefined directive that inherits from `default-src`, removing host source values when `*` is provided. Removing `'none'` when additional values are present, etc.):
52
+
53
+ ```ruby
54
+ def index
55
+ use_content_security_policy_named_append(:A)
56
+ use_content_security_policy_named_append(:B)
57
+ # produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval';
58
+ end
59
+
60
+ def show
61
+ use_content_security_policy_named_append(:B)
62
+ use_content_security_policy_named_append(:A)
63
+ # produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval';
64
+ end
65
+ ```
66
+
1
67
  ## 3.4.0 the frame-src/child-src transition for Firefox.
2
68
 
3
69
  Handle the `child-src`/`frame-src` transition semi-intelligently across versions. I think the code best descibes the behavior here:
data/Gemfile CHANGED
@@ -9,6 +9,7 @@ group :test do
9
9
  gem "rack", "~> 1"
10
10
  gem "rspec"
11
11
  gem "coveralls"
12
+ gem "term-ansicolor", "< 1.4"
12
13
  end
13
14
 
14
15
  group :guard do
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.png?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.png)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.png)](https://coveralls.io/r/twitter/secureheaders)
1
+ # Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.svg?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders)
2
2
 
3
3
 
4
4
  **The 3.x branch was recently merged**. See the [upgrading to 3.x doc](upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch.
@@ -20,10 +20,6 @@ It can also mark all http cookies with the Secure, HttpOnly and SameSite attribu
20
20
 
21
21
  `secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings.
22
22
 
23
- ## Use
24
-
25
- `gem install secure_headers`
26
-
27
23
  ## Configuration
28
24
 
29
25
  If you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block.
@@ -36,7 +32,7 @@ SecureHeaders::Configuration.default do |config|
36
32
  secure: true, # mark all cookies as "Secure"
37
33
  httponly: true, # mark all cookies as "HttpOnly"
38
34
  samesite: {
39
- strict: true # mark all cookies as SameSite=Strict
35
+ lax: true # mark all cookies as SameSite=lax
40
36
  }
41
37
  }
42
38
  config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload"
@@ -48,7 +44,7 @@ SecureHeaders::Configuration.default do |config|
48
44
  config.referrer_policy = "origin-when-cross-origin"
49
45
  config.csp = {
50
46
  # "meta" values. these will shaped the header, but the values are not included in the header.
51
- report_only: true, # default: false
47
+ report_only: true, # default: false [DEPRECATED from 3.5.0: instead, configure csp_report_only]
52
48
  preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
53
49
 
54
50
  # directive values: these values will directly translate into source directives
@@ -69,6 +65,11 @@ SecureHeaders::Configuration.default do |config|
69
65
  upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
70
66
  report_uri: %w(https://report-uri.io/example-csp)
71
67
  }
68
+ # This is available only from 3.5.0; use the `report_only: true` setting for 3.4.1 and below.
69
+ config.csp_report_only = config.csp.merge({
70
+ img_src: %w(somewhereelse.com),
71
+ report_uri: %w(https://report-uri.io/example-csp-report-only)
72
+ })
72
73
  config.hpkp = {
73
74
  report_only: false,
74
75
  max_age: 60.days.to_i,
@@ -92,7 +93,86 @@ use SecureHeaders::Middleware
92
93
 
93
94
  ## Default values
94
95
 
95
- All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers).
96
+ All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers). The default set of headers is:
97
+
98
+ ```
99
+ Content-Security-Policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'
100
+ Strict-Transport-Security: max-age=631138519
101
+ X-Content-Type-Options: nosniff
102
+ X-Download-Options: noopen
103
+ X-Frame-Options: sameorigin
104
+ X-Permitted-Cross-Domain-Policies: none
105
+ X-Xss-Protection: 1; mode=block
106
+ ```
107
+
108
+ ### Default CSP
109
+
110
+ By default, the above CSP will be applied to all requests. If you **only** want to set a Report-Only header, opt-out of the default enforced header for clarity. The configuration will assume that if you only supply `csp_report_only` that you intended to opt-out of `csp` but that's for the sake of backwards compatibility and it will be removed in the future.
111
+
112
+ ```ruby
113
+ Configuration.default do |config|
114
+ config.csp = SecureHeaders::OPT_OUT # If this line is omitted, we will assume you meant to opt out.
115
+ config.csp_report_only = {
116
+ default_src: %w('self')
117
+ }
118
+ end
119
+ ```
120
+
121
+ ## Named Appends
122
+
123
+ Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility.
124
+
125
+ ```ruby
126
+ def show
127
+ if include_widget?
128
+ @widget = widget.render
129
+ use_content_security_policy_named_append(:widget_partial)
130
+ end
131
+ end
132
+
133
+
134
+ SecureHeaders::Configuration.named_append(:widget_partial) do |request|
135
+ SecureHeaders.override_x_frame_options(request, "DENY")
136
+ if request.controller_instance.current_user.in_test_bucket?
137
+ { child_src: %w(beta.thirdpartyhost.com) }
138
+ else
139
+ { child_src: %w(thirdpartyhost.com) }
140
+ end
141
+ end
142
+ ```
143
+
144
+ You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following:
145
+
146
+ ```ruby
147
+ SecureHeader::Configuration.default do |config|
148
+ config.csp = { default_src: %w('self')}
149
+ end
150
+
151
+ SecureHeaders::Configuration.named_append(:A) do |request|
152
+ { default_src: %w(myhost.com) }
153
+ end
154
+
155
+ SecureHeaders::Configuration.named_append(:B) do |request|
156
+ { script_src: %w('unsafe-eval') }
157
+ end
158
+ ```
159
+
160
+ The following code will produce different policies due to the way policies are normalized (e.g. providing a previously undefined directive that inherits from `default-src`, removing host source values when `*` is provided. Removing `'none'` when additional values are present, etc.):
161
+
162
+ ```ruby
163
+ def index
164
+ use_content_security_policy_named_append(:A)
165
+ use_content_security_policy_named_append(:B)
166
+ # produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval';
167
+ end
168
+
169
+ def show
170
+ use_content_security_policy_named_append(:B)
171
+ use_content_security_policy_named_append(:A)
172
+ # produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval';
173
+ end
174
+ ```
175
+
96
176
 
97
177
  ## Named overrides
98
178
 
@@ -125,7 +205,7 @@ end
125
205
 
126
206
  class MyController < ApplicationController
127
207
  def index
128
- # Produces default-src 'self'; script-src example.org otherdomain.org
208
+ # Produces default-src 'self'; script-src example.org otherdomain.com
129
209
  use_secure_headers_override(:script_from_otherdomain_com)
130
210
  end
131
211
 
@@ -46,6 +46,17 @@ module SecureHeaders
46
46
  @configurations[name]
47
47
  end
48
48
 
49
+ def named_appends(name)
50
+ @appends ||= {}
51
+ @appends[name]
52
+ end
53
+
54
+ def named_append(name, target = nil, &block)
55
+ @appends ||= {}
56
+ raise "Provide a configuration block" unless block_given?
57
+ @appends[name] = block
58
+ end
59
+
49
60
  private
50
61
 
51
62
  # Private: add a valid configuration to the global set of named configs.
@@ -74,7 +85,6 @@ module SecureHeaders
74
85
  ALL_HEADER_CLASSES.each do |klass|
75
86
  config.send("#{klass::CONFIG_KEY}=", OPT_OUT)
76
87
  end
77
- config.dynamic_csp = OPT_OUT
78
88
  end
79
89
 
80
90
  add_configuration(NOOP_CONFIGURATION, noop_config)
@@ -83,6 +93,7 @@ module SecureHeaders
83
93
  # Public: perform a basic deep dup. The shallow copy provided by dup/clone
84
94
  # can lead to modifying parent objects.
85
95
  def deep_copy(config)
96
+ return unless config
86
97
  config.each_with_object({}) do |(key, value), hash|
87
98
  hash[key] = if value.is_a?(Array)
88
99
  value.dup
@@ -103,13 +114,11 @@ module SecureHeaders
103
114
  end
104
115
  end
105
116
 
106
- attr_accessor :dynamic_csp
107
-
108
117
  attr_writer :hsts, :x_frame_options, :x_content_type_options,
109
118
  :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
110
119
  :referrer_policy
111
120
 
112
- attr_reader :cached_headers, :csp, :cookies, :hpkp, :hpkp_report_host
121
+ attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
113
122
 
114
123
  HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
115
124
  if File.exists?(HASH_CONFIG_FILE)
@@ -121,7 +130,8 @@ module SecureHeaders
121
130
  def initialize(&block)
122
131
  self.hpkp = OPT_OUT
123
132
  self.referrer_policy = OPT_OUT
124
- self.csp = self.class.send(:deep_copy, CSP::DEFAULT_CONFIG)
133
+ self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT)
134
+ self.csp_report_only = OPT_OUT
125
135
  instance_eval &block if block_given?
126
136
  end
127
137
 
@@ -130,9 +140,9 @@ module SecureHeaders
130
140
  # Returns a deep-dup'd copy of this configuration.
131
141
  def dup
132
142
  copy = self.class.new
133
- copy.cookies = @cookies
134
- copy.csp = self.class.send(:deep_copy_if_hash, @csp)
135
- copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp)
143
+ copy.cookies = self.class.send(:deep_copy_if_hash, @cookies)
144
+ copy.csp = @csp.dup if @csp
145
+ copy.csp_report_only = @csp_report_only.dup if @csp_report_only
136
146
  copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
137
147
  copy.x_content_type_options = @x_content_type_options
138
148
  copy.hsts = @hsts
@@ -148,9 +158,6 @@ module SecureHeaders
148
158
 
149
159
  def opt_out(header)
150
160
  send("#{header}=", OPT_OUT)
151
- if header == CSP::CONFIG_KEY
152
- dynamic_csp = OPT_OUT
153
- end
154
161
  self.cached_headers.delete(header)
155
162
  end
156
163
 
@@ -159,20 +166,6 @@ module SecureHeaders
159
166
  self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value)
160
167
  end
161
168
 
162
- # Public: generated cached headers for a specific user agent.
163
- def rebuild_csp_header_cache!(user_agent)
164
- self.cached_headers[CSP::CONFIG_KEY] = {}
165
- unless current_csp == OPT_OUT
166
- user_agent = UserAgent.parse(user_agent)
167
- variation = CSP.ua_to_variation(user_agent)
168
- self.cached_headers[CSP::CONFIG_KEY][variation] = CSP.make_header(current_csp, user_agent)
169
- end
170
- end
171
-
172
- def current_csp
173
- @dynamic_csp || @csp
174
- end
175
-
176
169
  # Public: validates all configurations values.
177
170
  #
178
171
  # Raises various configuration errors if any invalid config is detected.
@@ -181,6 +174,7 @@ module SecureHeaders
181
174
  def validate_config!
182
175
  StrictTransportSecurity.validate_config!(@hsts)
183
176
  ContentSecurityPolicy.validate_config!(@csp)
177
+ ContentSecurityPolicy.validate_config!(@csp_report_only)
184
178
  ReferrerPolicy.validate_config!(@referrer_policy)
185
179
  XFrameOptions.validate_config!(@x_frame_options)
186
180
  XContentTypeOptions.validate_config!(@x_content_type_options)
@@ -196,16 +190,50 @@ module SecureHeaders
196
190
  @cookies = (@cookies || {}).merge(secure: secure_cookies)
197
191
  end
198
192
 
199
- protected
200
-
201
193
  def csp=(new_csp)
202
- if self.dynamic_csp
203
- raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead."
194
+ if new_csp.respond_to?(:opt_out?)
195
+ @csp = new_csp.dup
196
+ else
197
+ if new_csp[:report_only]
198
+ # Deprecated configuration implies that CSPRO should be set, CSP should not - so opt out
199
+ Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp=` was supplied a config with report_only: true. Use #csp_report_only="
200
+ @csp = OPT_OUT
201
+ self.csp_report_only = new_csp
202
+ else
203
+ @csp = ContentSecurityPolicyConfig.new(new_csp)
204
+ end
205
+ end
206
+ end
207
+
208
+ # Configures the Content-Security-Policy-Report-Only header. `new_csp` cannot
209
+ # contain `report_only: false` or an error will be raised.
210
+ #
211
+ # NOTE: if csp has not been configured/has the default value when
212
+ # configuring csp_report_only, the code will assume you mean to only use
213
+ # report-only mode and you will be opted-out of enforce mode.
214
+ def csp_report_only=(new_csp)
215
+ @csp_report_only = begin
216
+ if new_csp.is_a?(ContentSecurityPolicyConfig)
217
+ new_csp.make_report_only
218
+ elsif new_csp.respond_to?(:opt_out?)
219
+ new_csp.dup
220
+ else
221
+ if new_csp[:report_only] == false # nil is a valid value on which we do not want to raise
222
+ raise ContentSecurityPolicyConfigError, "`#csp_report_only=` was supplied a config with report_only: false. Use #csp="
223
+ else
224
+ ContentSecurityPolicyReportOnlyConfig.new(new_csp)
225
+ end
226
+ end
204
227
  end
205
228
 
206
- @csp = self.class.send(:deep_copy_if_hash, new_csp)
229
+ if !@csp_report_only.opt_out? && @csp.to_h == ContentSecurityPolicyConfig::DEFAULT
230
+ Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp_report_only=` was configured before `#csp=`. It is assumed you intended to opt out of `#csp=` so be sure to add `config.csp = SecureHeaders::OPT_OUT` to your config. Ensure that #csp_report_only is configured after #csp="
231
+ @csp = OPT_OUT
232
+ end
207
233
  end
208
234
 
235
+ protected
236
+
209
237
  def cookies=(cookies)
210
238
  @cookies = cookies
211
239
  end
@@ -258,12 +286,16 @@ module SecureHeaders
258
286
  #
259
287
  # Returns nothing
260
288
  def generate_csp_headers(headers)
261
- unless @csp == OPT_OUT
262
- headers[CSP::CONFIG_KEY] = {}
263
- csp_config = self.current_csp
264
- CSP::VARIATIONS.each do |name, _|
265
- csp = CSP.make_header(csp_config, UserAgent.parse(name))
266
- headers[CSP::CONFIG_KEY][name] = csp.freeze
289
+ generate_csp_headers_for_config(headers, ContentSecurityPolicyConfig::CONFIG_KEY, self.csp)
290
+ generate_csp_headers_for_config(headers, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY, self.csp_report_only)
291
+ end
292
+
293
+ def generate_csp_headers_for_config(headers, header_key, csp_config)
294
+ unless csp_config.opt_out?
295
+ headers[header_key] = {}
296
+ ContentSecurityPolicy::VARIATIONS.each do |name, _|
297
+ csp = ContentSecurityPolicy.make_header(csp_config, UserAgent.parse(name))
298
+ headers[header_key][name] = csp.freeze
267
299
  end
268
300
  end
269
301
  end
@@ -1,37 +1,44 @@
1
1
  require_relative 'policy_management'
2
+ require_relative 'content_security_policy_config'
2
3
  require 'useragent'
3
4
 
4
5
  module SecureHeaders
5
- class ContentSecurityPolicyConfigError < StandardError; end
6
6
  class ContentSecurityPolicy
7
7
  include PolicyManagement
8
8
 
9
9
  # constants to be used for version-specific UA sniffing
10
10
  VERSION_46 = ::UserAgent::Version.new("46")
11
+ VERSION_10 = ::UserAgent::Version.new("10")
11
12
 
12
13
  def initialize(config = nil, user_agent = OTHER)
13
- @config = Configuration.send(:deep_copy, config || DEFAULT_CONFIG)
14
+ @config = if config.is_a?(Hash)
15
+ if config[:report_only]
16
+ ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
17
+ else
18
+ ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG)
19
+ end
20
+ elsif config.nil?
21
+ ContentSecurityPolicyConfig.new(DEFAULT_CONFIG)
22
+ else
23
+ config
24
+ end
25
+
14
26
  @parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
15
27
  user_agent
16
28
  else
17
29
  UserAgent.parse(user_agent)
18
30
  end
19
- normalize_child_frame_src
20
- @report_only = @config[:report_only]
21
- @preserve_schemes = @config[:preserve_schemes]
22
- @script_nonce = @config[:script_nonce]
23
- @style_nonce = @config[:style_nonce]
31
+ @frame_src = normalize_child_frame_src
32
+ @preserve_schemes = @config.preserve_schemes
33
+ @script_nonce = @config.script_nonce
34
+ @style_nonce = @config.style_nonce
24
35
  end
25
36
 
26
37
  ##
27
38
  # Returns the name to use for the header. Either "Content-Security-Policy" or
28
39
  # "Content-Security-Policy-Report-Only"
29
40
  def name
30
- if @report_only
31
- REPORT_ONLY
32
- else
33
- HEADER_NAME
34
- end
41
+ @config.class.const_get(:HEADER_NAME)
35
42
  end
36
43
 
37
44
  ##
@@ -49,16 +56,16 @@ module SecureHeaders
49
56
  # frame-src is deprecated, child-src is being implemented. They are
50
57
  # very similar and in most cases, the same value can be used for both.
51
58
  def normalize_child_frame_src
52
- if @config[:frame_src] && @config[:child_src] && @config[:frame_src] != @config[:child_src]
59
+ if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src
53
60
  Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers.")
54
- elsif @config[:frame_src]
55
- Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config[:frame_src]}.")
61
+ elsif @config.frame_src
62
+ Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config.frame_src}.")
56
63
  end
57
64
 
58
65
  if supported_directives.include?(:child_src)
59
- @config[:child_src] = @config[:child_src] || @config[:frame_src]
66
+ @config.child_src || @config.frame_src
60
67
  else
61
- @config[:frame_src] = @config[:frame_src] || @config[:child_src]
68
+ @config.frame_src || @config.child_src
62
69
  end
63
70
  end
64
71
 
@@ -73,9 +80,9 @@ module SecureHeaders
73
80
  directives.map do |directive_name|
74
81
  case DIRECTIVE_VALUE_TYPES[directive_name]
75
82
  when :boolean
76
- symbol_to_hyphen_case(directive_name) if @config[directive_name]
83
+ symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
77
84
  when :string
78
- [symbol_to_hyphen_case(directive_name), @config[directive_name]].join(" ")
85
+ [symbol_to_hyphen_case(directive_name), @config.directive_value(directive_name)].join(" ")
79
86
  else
80
87
  build_directive(directive_name)
81
88
  end
@@ -88,11 +95,19 @@ module SecureHeaders
88
95
  #
89
96
  # Returns a string representing a directive.
90
97
  def build_directive(directive)
91
- return if @config[directive].nil?
92
-
93
- source_list = @config[directive].compact
94
- return if source_list.empty?
95
-
98
+ source_list = case directive
99
+ when :child_src
100
+ if supported_directives.include?(:child_src)
101
+ @frame_src
102
+ end
103
+ when :frame_src
104
+ unless supported_directives.include?(:child_src)
105
+ @frame_src
106
+ end
107
+ else
108
+ @config.directive_value(directive)
109
+ end
110
+ return unless source_list && source_list.any?
96
111
  normalized_source_list = minify_source_list(directive, source_list)
97
112
  [symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
98
113
  end
@@ -101,16 +116,17 @@ module SecureHeaders
101
116
  # If a directive contains 'none' but has other values, 'none' is ommitted.
102
117
  # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
103
118
  def minify_source_list(directive, source_list)
119
+ source_list = source_list.compact
104
120
  if source_list.include?(STAR)
105
121
  keep_wildcard_sources(source_list)
106
122
  else
107
- populate_nonces!(directive, source_list)
108
- reject_all_values_if_none!(source_list)
123
+ source_list = populate_nonces(directive, source_list)
124
+ source_list = reject_all_values_if_none(source_list)
109
125
 
110
126
  unless directive == REPORT_URI || @preserve_schemes
111
- strip_source_schemes!(source_list)
127
+ source_list = strip_source_schemes(source_list)
112
128
  end
113
- dedup_source_list(source_list).join(" ")
129
+ dedup_source_list(source_list)
114
130
  end
115
131
  end
116
132
 
@@ -120,8 +136,12 @@ module SecureHeaders
120
136
  end
121
137
 
122
138
  # Discard any 'none' values if more directives are supplied since none may override values.
123
- def reject_all_values_if_none!(source_list)
124
- source_list.reject! { |value| value == NONE } if source_list.length > 1
139
+ def reject_all_values_if_none(source_list)
140
+ if source_list.length > 1
141
+ source_list.reject { |value| value == NONE }
142
+ else
143
+ source_list
144
+ end
125
145
  end
126
146
 
127
147
  # Removes duplicates and sources that already match an existing wild card.
@@ -143,12 +163,14 @@ module SecureHeaders
143
163
 
144
164
  # Private: append a nonce to the script/style directories if script_nonce
145
165
  # or style_nonce are provided.
146
- def populate_nonces!(directive, source_list)
166
+ def populate_nonces(directive, source_list)
147
167
  case directive
148
168
  when SCRIPT_SRC
149
169
  append_nonce(source_list, @script_nonce)
150
170
  when STYLE_SRC
151
171
  append_nonce(source_list, @style_nonce)
172
+ else
173
+ source_list
152
174
  end
153
175
  end
154
176
 
@@ -165,19 +187,23 @@ module SecureHeaders
165
187
  source_list << UNSAFE_INLINE
166
188
  end
167
189
  end
190
+
191
+ source_list
168
192
  end
169
193
 
170
194
  # Private: return the list of directives that are supported by the user agent,
171
195
  # starting with default-src and ending with report-uri.
172
196
  def directives
173
- [DEFAULT_SRC,
197
+ [
198
+ DEFAULT_SRC,
174
199
  BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
175
- REPORT_URI].flatten.select { |directive| @config.key?(directive) }
200
+ REPORT_URI
201
+ ].flatten
176
202
  end
177
203
 
178
204
  # Private: Remove scheme from source expressions.
179
- def strip_source_schemes!(source_list)
180
- source_list.map! { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
205
+ def strip_source_schemes(source_list)
206
+ source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
181
207
  end
182
208
 
183
209
  # Private: determine which directives are supported for the given user agent.
@@ -198,7 +224,7 @@ module SecureHeaders
198
224
  end
199
225
 
200
226
  def nonces_supported?
201
- @nonces_supported ||= MODERN_BROWSERS.include?(@parsed_ua.browser)
227
+ @nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
202
228
  end
203
229
 
204
230
  def symbol_to_hyphen_case(sym)