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 +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
|