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 +4 -4
- data/README.md +2 -1
- data/lib/secure_headers/headers/content_security_policy.rb +111 -48
- data/lib/secure_headers/version.rb +1 -1
- data/lib/secure_headers/view_helper.rb +0 -1
- data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +0 -1
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +26 -4
- data/spec/lib/secure_headers_spec.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 333c51c1bbbed7415696fa0e8fd7e3147ddcf542
|
4
|
+
data.tar.gz: 798eae0a9dc303a0c72c946e74aa76a2f36e229c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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.
|
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
|
-
|
169
|
-
|
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
|
-
|
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?
|
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
|
-
|
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
|
-
|
247
|
-
|
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
|
258
|
-
|
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
|
@@ -5,9 +5,10 @@ module SecureHeaders
|
|
5
5
|
let(:default_opts) do
|
6
6
|
{
|
7
7
|
:default_src => 'https:',
|
8
|
-
:
|
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:"],"
|
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
|
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:"
|
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:"
|
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.
|
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-
|
11
|
+
date: 2015-10-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|