secure_headers 2.4.0 → 2.4.1

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.

Potentially problematic release.


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

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bdc7e4cc47367102f2318244051b544e4bc5d611
4
- data.tar.gz: f4bb9651462b15c056ab9720a0c079283e9c2462
3
+ metadata.gz: 333c51c1bbbed7415696fa0e8fd7e3147ddcf542
4
+ data.tar.gz: 798eae0a9dc303a0c72c946e74aa76a2f36e229c
5
5
  SHA512:
6
- metadata.gz: 888729b84ba1eb266c25c3bbc218c270f9e262bcbf490363a2daad6202803f4ba71a21f59c1a36e816a06c01ab8d28126ba81e05ae4f37ac36a53ee670af8420
7
- data.tar.gz: 26640f0a917396faf083eaff557316d4a376e6956ae95915cfb5c815de3269a6ec5cb9a9f6a7684d871d8ff634dc586bb6a6a20dd79ece6cee034d5ec8f20648
6
+ metadata.gz: 80224aff2b4a8230de68b0338dd5ca4ef19c56b929fe59693d3f01395f7b4c3cab57170b389044dacd0745e43d07cf98951f13de48d646cc17cf8a40befe4d90
7
+ data.tar.gz: 7f663e76802e8c2282908eb2eefce7a040cf9f5c04da49b825bd147854879a900577f22f9a8e918a973d3019e840e95d08aec6eaeeb3836c6233fb0db0dd8982
data/README.md CHANGED
@@ -61,6 +61,7 @@ The following methods are going to be called, unless they are provided in a `ski
61
61
  :form_action => "'self' github.com",
62
62
  :frame_ancestors => "'none'",
63
63
  :plugin_types => 'application/x-shockwave-flash',
64
+ :block_all_mixed_content => '' # see [http://www.w3.org/TR/mixed-content/]()
64
65
  :report_uri => '//example.com/uri-directive'
65
66
  }
66
67
  config.hpkp = {
@@ -99,7 +100,7 @@ Sometimes you need to override your content security policy for a given endpoint
99
100
  1. Override the `secure_header_options_for` class instance method. e.g.
100
101
 
101
102
  ```ruby
102
- class SomethingController < ApplicationController
103
+ class SomethingController < ApplicationController
103
104
  def wumbus
104
105
  # gets style-src override
105
106
  end
@@ -11,7 +11,8 @@ module SecureHeaders
11
11
  DEFAULT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https: about: javascript:; img-src data:"
12
12
  HEADER_NAME = "Content-Security-Policy"
13
13
  ENV_KEY = 'secure_headers.content_security_policy'
14
- DIRECTIVES = [
14
+
15
+ DIRECTIVES_1_0 = [
15
16
  :default_src,
16
17
  :connect_src,
17
18
  :font_src,
@@ -19,20 +20,53 @@ module SecureHeaders
19
20
  :img_src,
20
21
  :media_src,
21
22
  :object_src,
23
+ :sandbox,
22
24
  :script_src,
23
25
  :style_src,
26
+ :report_uri
27
+ ].freeze
28
+
29
+ DIRECTIVES_2_0 = [
30
+ DIRECTIVES_1_0,
24
31
  :base_uri,
25
32
  :child_src,
26
33
  :form_action,
27
34
  :frame_ancestors,
28
35
  :plugin_types
29
- ]
36
+ ].flatten.freeze
30
37
 
31
- OTHER = [
32
- :report_uri
33
- ]
34
38
 
35
- ALL_DIRECTIVES = DIRECTIVES + OTHER
39
+ # All the directives currently under consideration for CSP level 3.
40
+ # https://w3c.github.io/webappsec/specs/CSP2/
41
+ DIRECTIVES_3_0 = [
42
+ DIRECTIVES_2_0,
43
+ :manifest_src,
44
+ :reflected_xss
45
+ ].flatten.freeze
46
+
47
+ # All the directives that are not currently in a formal spec, but have
48
+ # been implemented somewhere.
49
+ DIRECTIVES_DRAFT = [
50
+ :block_all_mixed_content,
51
+ ].freeze
52
+
53
+ SAFARI_DIRECTIVES = DIRECTIVES_1_0
54
+
55
+ FIREFOX_UNSUPPORTED_DIRECTIVES = [
56
+ :block_all_mixed_content,
57
+ :child_src,
58
+ :plugin_types
59
+ ].freeze
60
+
61
+ FIREFOX_DIRECTIVES = (
62
+ DIRECTIVES_2_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
63
+ ).freeze
64
+
65
+ CHROME_DIRECTIVES = (
66
+ DIRECTIVES_2_0 + DIRECTIVES_DRAFT
67
+ ).freeze
68
+
69
+ ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort
36
70
  CONFIG_KEY = :csp
37
71
  end
38
72
 
@@ -99,33 +133,55 @@ module SecureHeaders
99
133
  @ua = options[:ua]
100
134
  @ssl_request = !!options.delete(:ssl)
101
135
  @request_uri = options.delete(:request_uri)
136
+ @http_additions = config.delete(:http_additions)
137
+ @disable_img_src_data_uri = !!config.delete(:disable_img_src_data_uri)
138
+ @tag_report_uri = !!config.delete(:tag_report_uri)
139
+ @script_hashes = config.delete(:script_hashes) || []
140
+ @app_name = config.delete(:app_name)
141
+ @app_name = @app_name.call(@controller) if @app_name.respond_to?(:call)
142
+ @enforce = config.delete(:enforce)
143
+ @enforce = @enforce.call(@controller) if @enforce.respond_to?(:call)
144
+ @enforce = !!@enforce
102
145
 
103
146
  # Config values can be string, array, or lamdba values
104
147
  @config = config.inject({}) do |hash, (key, value)|
105
148
  config_val = value.respond_to?(:call) ? value.call(@controller) : value
106
-
107
- if DIRECTIVES.include?(key) # directives need to be normalized to arrays of strings
149
+ if ALL_DIRECTIVES.include?(key.to_sym) # directives need to be normalized to arrays of strings
108
150
  config_val = config_val.split if config_val.is_a? String
109
151
  if config_val.is_a?(Array)
110
152
  config_val = config_val.map do |val|
111
153
  translate_dir_value(val)
112
154
  end.flatten.uniq
113
155
  end
156
+ elsif key != :script_hash_middleware
157
+ raise ArgumentError.new("Unknown directive supplied: #{key}")
114
158
  end
115
159
 
116
160
  hash[key] = config_val
117
161
  hash
118
162
  end
119
163
 
120
- @http_additions = @config.delete(:http_additions)
121
- @app_name = @config.delete(:app_name)
122
- @report_uri = @config.delete(:report_uri)
123
- @enforce = !!@config.delete(:enforce)
124
- @disable_img_src_data_uri = !!@config.delete(:disable_img_src_data_uri)
125
- @tag_report_uri = !!@config.delete(:tag_report_uri)
126
- @script_hashes = @config.delete(:script_hashes) || []
164
+ # normalize and tag the report-uri
165
+ if @config[:report_uri]
166
+ @config[:report_uri] = @config[:report_uri].map do |report_uri|
167
+ if report_uri.start_with?('//')
168
+ report_uri = if @ssl_request
169
+ "https:" + report_uri
170
+ else
171
+ "http:" + report_uri
172
+ end
173
+ end
174
+
175
+ if @tag_report_uri
176
+ report_uri = "#{report_uri}?enforce=#{@enforce}"
177
+ report_uri += "&app_name=#{@app_name}" if @app_name
178
+ end
179
+ report_uri
180
+ end
181
+ end
127
182
 
128
183
  add_script_hashes if @script_hashes.any?
184
+ strip_unsupported_directives
129
185
  end
130
186
 
131
187
  ##
@@ -160,13 +216,20 @@ module SecureHeaders
160
216
 
161
217
  def to_json
162
218
  build_value
163
- @config.to_json.gsub(/(\w+)_src/, "\\1-src")
219
+ @config.inject({}) do |hash, (key, value)|
220
+ if ALL_DIRECTIVES.include?(key)
221
+ hash[key.to_s.gsub(/(\w+)_(\w+)/, "\\1-\\2")] = value
222
+ end
223
+ hash
224
+ end.to_json
164
225
  end
165
226
 
166
227
  def self.from_json(*json_configs)
167
228
  json_configs.inject({}) do |combined_config, one_config|
168
- one_config = one_config.gsub(/(\w+)-src/, "\\1_src")
169
- config = JSON.parse(one_config, :symbolize_names => true)
229
+ config = JSON.parse(one_config).inject({}) do |hash, (key, value)|
230
+ hash[key.gsub(/(\w+)-(\w+)/, "\\1_\\2").to_sym] = value
231
+ hash
232
+ end
170
233
  combined_config.merge(config) do |_, lhs, rhs|
171
234
  lhs | rhs
172
235
  end
@@ -182,10 +245,7 @@ module SecureHeaders
182
245
  def build_value
183
246
  raise "Expected to find default_src directive value" unless @config[:default_src]
184
247
  append_http_additions unless ssl_request?
185
- header_value = [
186
- generic_directives,
187
- report_uri_directive
188
- ].join.strip
248
+ generic_directives
189
249
  end
190
250
 
191
251
  def append_http_additions
@@ -204,7 +264,7 @@ module SecureHeaders
204
264
  warn "[DEPRECATION] using self/none may not be supported in the future. Instead use 'self'/'none' instead."
205
265
  "'#{val}'"
206
266
  elsif val == 'nonce'
207
- if supports_nonces?(@ua)
267
+ if supports_nonces?
208
268
  self.class.set_nonce(@controller, nonce)
209
269
  ["'nonce-#{nonce}'", "'unsafe-inline'"]
210
270
  else
@@ -215,27 +275,9 @@ module SecureHeaders
215
275
  end
216
276
  end
217
277
 
218
- def report_uri_directive
219
- return '' if @report_uri.nil?
220
-
221
- if @report_uri.start_with?('//')
222
- @report_uri = if @ssl_request
223
- "https:" + @report_uri
224
- else
225
- "http:" + @report_uri
226
- end
227
- end
228
-
229
- if @tag_report_uri
230
- @report_uri = "#{@report_uri}?enforce=#{@enforce}"
231
- @report_uri += "&app_name=#{@app_name}" if @app_name
232
- end
233
-
234
- "report-uri #{@report_uri};"
235
- end
236
-
278
+ # ensures defualt_src is first and report_uri is last
237
279
  def generic_directives
238
- header_value = ''
280
+ header_value = build_directive(:default_src)
239
281
  data_uri = @disable_img_src_data_uri ? [] : ["data:"]
240
282
  if @config[:img_src]
241
283
  @config[:img_src] = @config[:img_src] + data_uri unless @config[:img_src].include?('data:')
@@ -243,19 +285,40 @@ module SecureHeaders
243
285
  @config[:img_src] = @config[:default_src] + data_uri
244
286
  end
245
287
 
246
- DIRECTIVES.each do |directive_name|
247
- header_value += build_directive(directive_name) if @config[directive_name]
288
+ (ALL_DIRECTIVES - [:default_src, :report_uri]).each do |directive_name|
289
+ if @config[directive_name]
290
+ header_value += build_directive(directive_name)
291
+ end
248
292
  end
249
293
 
250
- header_value
294
+ header_value += build_directive(:report_uri) if @config[:report_uri]
295
+
296
+ header_value.strip
251
297
  end
252
298
 
253
299
  def build_directive(key)
254
300
  "#{self.class.symbol_to_hyphen_case(key)} #{@config[key].join(" ")}; "
255
301
  end
256
302
 
257
- def supports_nonces?(user_agent)
258
- parsed_ua = UserAgentParser.parse(user_agent)
303
+ def strip_unsupported_directives
304
+ @config.select! { |key, _| supported_directives.include?(key) }
305
+ end
306
+
307
+ def supported_directives
308
+ @supported_directives ||= case UserAgentParser.parse(@ua).family
309
+ when "Chrome"
310
+ CHROME_DIRECTIVES
311
+ when "Safari"
312
+ SAFARI_DIRECTIVES
313
+ when "Firefox"
314
+ FIREFOX_DIRECTIVES
315
+ else
316
+ DIRECTIVES_1_0
317
+ end
318
+ end
319
+
320
+ def supports_nonces?
321
+ parsed_ua = UserAgentParser.parse(@ua)
259
322
  ["Chrome", "Opera", "Firefox"].include?(parsed_ua.family)
260
323
  end
261
324
  end
@@ -1,3 +1,3 @@
1
1
  module SecureHeaders
2
- VERSION = "2.4.0"
2
+ VERSION = "2.4.1"
3
3
  end
@@ -27,7 +27,6 @@ module SecureHeaders
27
27
  if raise_error_on_unrecognized_hash
28
28
  raise UnexpectedHashedScriptException.new(message)
29
29
  else
30
- puts message
31
30
  request.env[HASHES_ENV_KEY] = (request.env[HASHES_ENV_KEY] || []) << hash_value
32
31
  end
33
32
  end
@@ -10,7 +10,6 @@ module SecureHeaders
10
10
 
11
11
  let(:default_config) do
12
12
  {
13
- :disable_fill_missing => true,
14
13
  :default_src => 'https://*',
15
14
  :report_uri => '/csp_report',
16
15
  :script_src => "'unsafe-inline' 'unsafe-eval' https://* data:",
@@ -5,9 +5,10 @@ module SecureHeaders
5
5
  let(:default_opts) do
6
6
  {
7
7
  :default_src => 'https:',
8
- :report_uri => '/csp_report',
8
+ :img_src => "https: data:",
9
9
  :script_src => "'unsafe-inline' 'unsafe-eval' https: data:",
10
- :style_src => "'unsafe-inline' https: about:"
10
+ :style_src => "'unsafe-inline' https: about:",
11
+ :report_uri => '/csp_report'
11
12
  }
12
13
  end
13
14
  let(:controller) { DummyClass.new }
@@ -58,7 +59,7 @@ module SecureHeaders
58
59
 
59
60
  it "exports a policy to JSON" do
60
61
  policy = ContentSecurityPolicy.new(default_opts)
61
- expected = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"img-src":["https:","data:"]})
62
+ expected = %({"default-src":["https:"],"img-src":["https:","data:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"report-uri":["/csp_report"]})
62
63
  expect(policy.to_json).to eq(expected)
63
64
  end
64
65
 
@@ -141,6 +142,27 @@ module SecureHeaders
141
142
  end
142
143
 
143
144
  describe "#value" do
145
+ context "browser sniffing" do
146
+ let(:complex_opts) do
147
+ ALL_DIRECTIVES.inject({}) { |memo, directive| memo[directive] = "'self'"; memo }.merge(:block_all_mixed_content => '')
148
+ end
149
+
150
+ it "does not filter any directives for Chrome" do
151
+ policy = ContentSecurityPolicy.new(complex_opts, :request => request_for(CHROME))
152
+ expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content ; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';")
153
+ end
154
+
155
+ it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
156
+ policy = ContentSecurityPolicy.new(complex_opts, :request => request_for(FIREFOX))
157
+ expect(policy.value).to eq("default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';")
158
+ end
159
+
160
+ it "filters base-uri, blocked-all-mixed-content, child-src, form-action, frame-ancestors, and plugin-types for safari" do
161
+ policy = ContentSecurityPolicy.new(complex_opts, :request => request_for(SAFARI))
162
+ expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';")
163
+ end
164
+ end
165
+
144
166
  it "raises an exception when default-src is missing" do
145
167
  csp = ContentSecurityPolicy.new({:script_src => 'anything'}, :request => request_for(CHROME))
146
168
  expect {
@@ -248,7 +270,7 @@ module SecureHeaders
248
270
 
249
271
  it "adds directive values for headers on http" do
250
272
  csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
251
- expect(csp.value).to eq("default-src https:; frame-src http:; img-src http: data:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;")
273
+ expect(csp.value).to eq("default-src https:; frame-src http:; img-src https: data: http:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;")
252
274
  end
253
275
 
254
276
  it "does not add the directive values if requesting https" do
@@ -166,7 +166,7 @@ describe SecureHeaders do
166
166
  end
167
167
 
168
168
  it "produces a hash of headers given a hash as config" do
169
- hash = SecureHeaders::header_hash(:csp => {:default_src => "'none'", :img_src => "data:", :disable_fill_missing => true})
169
+ hash = SecureHeaders::header_hash(:csp => {:default_src => "'none'", :img_src => "data:"})
170
170
  expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'none'; img-src data:;")
171
171
  expect_default_values(hash)
172
172
  end
@@ -186,7 +186,7 @@ describe SecureHeaders do
186
186
  }
187
187
  end
188
188
 
189
- hash = SecureHeaders::header_hash(:csp => {:default_src => "'none'", :img_src => "data:", :disable_fill_missing => true})
189
+ hash = SecureHeaders::header_hash(:csp => {:default_src => "'none'", :img_src => "data:"})
190
190
  ::SecureHeaders::Configuration.configure do |config|
191
191
  config.hsts = nil
192
192
  config.hpkp = nil
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: secure_headers
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Neil Matatall
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-10-01 00:00:00.000000000 Z
11
+ date: 2015-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake