secure_headers 2.4.0 → 2.4.1

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

Potentially problematic release.


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

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