secure_headers 3.0.0 → 3.1.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 +4 -4
- data/.gitignore +0 -9
- data/.travis.yml +13 -5
- data/CHANGELOG.md +38 -0
- data/Gemfile +5 -2
- data/README.md +11 -45
- data/lib/secure_headers/configuration.rb +85 -54
- data/lib/secure_headers/headers/content_security_policy.rb +31 -294
- data/lib/secure_headers/headers/policy_management.rb +319 -0
- data/lib/secure_headers/headers/x_content_type_options.rb +1 -1
- data/lib/secure_headers/middleware.rb +23 -0
- data/lib/secure_headers/railtie.rb +3 -3
- data/lib/secure_headers.rb +37 -63
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/configuration_spec.rb +2 -4
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +13 -143
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +190 -0
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +1 -1
- data/spec/lib/secure_headers/middleware_spec.rb +23 -4
- data/spec/lib/secure_headers_spec.rb +100 -41
- data/spec/spec_helper.rb +4 -1
- data/upgrading-to-3-0.md +1 -0
- metadata +5 -4
- data/lib/secure_headers/padrino.rb +0 -13
- data/travis.sh +0 -10
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module SecureHeaders
|
|
2
2
|
class Middleware
|
|
3
|
+
SECURE_COOKIE_REGEXP = /;\s*secure\s*(;|$)/i.freeze
|
|
4
|
+
|
|
3
5
|
def initialize(app)
|
|
4
6
|
@app = app
|
|
5
7
|
end
|
|
@@ -8,8 +10,29 @@ module SecureHeaders
|
|
|
8
10
|
def call(env)
|
|
9
11
|
req = Rack::Request.new(env)
|
|
10
12
|
status, headers, response = @app.call(env)
|
|
13
|
+
|
|
14
|
+
config = SecureHeaders.config_for(req)
|
|
15
|
+
flag_cookies_as_secure!(headers) if config.secure_cookies
|
|
11
16
|
headers.merge!(SecureHeaders.header_hash_for(req))
|
|
12
17
|
[status, headers, response]
|
|
13
18
|
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194
|
|
23
|
+
def flag_cookies_as_secure!(headers)
|
|
24
|
+
if cookies = headers['Set-Cookie']
|
|
25
|
+
# Support Rails 2.3 / Rack 1.1 arrays as headers
|
|
26
|
+
cookies = cookies.split("\n") unless cookies.is_a?(Array)
|
|
27
|
+
|
|
28
|
+
headers['Set-Cookie'] = cookies.map do |cookie|
|
|
29
|
+
if cookie !~ SECURE_COOKIE_REGEXP
|
|
30
|
+
"#{cookie}; secure"
|
|
31
|
+
else
|
|
32
|
+
cookie
|
|
33
|
+
end
|
|
34
|
+
end.join("\n")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
14
37
|
end
|
|
15
38
|
end
|
|
@@ -3,14 +3,14 @@ if defined?(Rails::Railtie)
|
|
|
3
3
|
module SecureHeaders
|
|
4
4
|
class Railtie < Rails::Railtie
|
|
5
5
|
isolate_namespace SecureHeaders if defined? isolate_namespace # rails 3.0
|
|
6
|
-
conflicting_headers = ['X-Frame-Options', 'X-XSS-Protection',
|
|
6
|
+
conflicting_headers = ['X-Frame-Options', 'X-XSS-Protection',
|
|
7
7
|
'X-Permitted-Cross-Domain-Policies', 'X-Download-Options',
|
|
8
8
|
'X-Content-Type-Options', 'Strict-Transport-Security',
|
|
9
9
|
'Content-Security-Policy', 'Content-Security-Policy-Report-Only',
|
|
10
|
-
'
|
|
10
|
+
'Public-Key-Pins', 'Public-Key-Pins-Report-Only']
|
|
11
11
|
|
|
12
12
|
initializer "secure_headers.middleware" do
|
|
13
|
-
Rails.application.config.middleware.
|
|
13
|
+
Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
initializer "secure_headers.action_controller" do
|
data/lib/secure_headers.rb
CHANGED
|
@@ -48,14 +48,12 @@ module SecureHeaders
|
|
|
48
48
|
# script_src: %w(another-host.com)
|
|
49
49
|
def override_content_security_policy_directives(request, additions)
|
|
50
50
|
config = config_for(request)
|
|
51
|
-
|
|
52
|
-
config =
|
|
53
|
-
if config.csp == OPT_OUT
|
|
54
|
-
config.csp = {}
|
|
55
|
-
end
|
|
56
|
-
config.csp.merge!(additions)
|
|
57
|
-
override_secure_headers_request_config(request, config)
|
|
51
|
+
if config.current_csp == OPT_OUT
|
|
52
|
+
config.dynamic_csp = {}
|
|
58
53
|
end
|
|
54
|
+
|
|
55
|
+
config.dynamic_csp = config.current_csp.merge(additions)
|
|
56
|
+
override_secure_headers_request_config(request, config)
|
|
59
57
|
end
|
|
60
58
|
|
|
61
59
|
# Public: appends source values to the current configuration. If no value
|
|
@@ -66,11 +64,8 @@ module SecureHeaders
|
|
|
66
64
|
# script_src: %w(another-host.com)
|
|
67
65
|
def append_content_security_policy_directives(request, additions)
|
|
68
66
|
config = config_for(request)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
config.csp = CSP.combine_policies(config.csp, additions)
|
|
72
|
-
override_secure_headers_request_config(request, config)
|
|
73
|
-
end
|
|
67
|
+
config.dynamic_csp = CSP.combine_policies(config.current_csp, additions)
|
|
68
|
+
override_secure_headers_request_config(request, config)
|
|
74
69
|
end
|
|
75
70
|
|
|
76
71
|
# Public: override X-Frame-Options settings for this request.
|
|
@@ -79,16 +74,16 @@ module SecureHeaders
|
|
|
79
74
|
#
|
|
80
75
|
# Returns the current config
|
|
81
76
|
def override_x_frame_options(request, value)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
override_secure_headers_request_config(request,
|
|
77
|
+
config = config_for(request)
|
|
78
|
+
config.update_x_frame_options(value)
|
|
79
|
+
override_secure_headers_request_config(request, config)
|
|
85
80
|
end
|
|
86
81
|
|
|
87
82
|
# Public: opts out of setting a given header by creating a temporary config
|
|
88
83
|
# and setting the given headers config to OPT_OUT.
|
|
89
84
|
def opt_out_of_header(request, header_key)
|
|
90
|
-
config = config_for(request)
|
|
91
|
-
config.
|
|
85
|
+
config = config_for(request)
|
|
86
|
+
config.opt_out(header_key)
|
|
92
87
|
override_secure_headers_request_config(request, config)
|
|
93
88
|
end
|
|
94
89
|
|
|
@@ -109,14 +104,11 @@ module SecureHeaders
|
|
|
109
104
|
# in Rack middleware.
|
|
110
105
|
def header_hash_for(request)
|
|
111
106
|
config = config_for(request)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
use_cached_headers(cached_headers, request)
|
|
115
|
-
else
|
|
116
|
-
build_headers(config, request)
|
|
107
|
+
unless ContentSecurityPolicy.idempotent_additions?(config.csp, config.current_csp)
|
|
108
|
+
config.rebuild_csp_header_cache!(request.user_agent)
|
|
117
109
|
end
|
|
118
110
|
|
|
119
|
-
|
|
111
|
+
use_cached_headers(config.cached_headers, request)
|
|
120
112
|
end
|
|
121
113
|
|
|
122
114
|
# Public: specify which named override will be used for this request.
|
|
@@ -149,6 +141,22 @@ module SecureHeaders
|
|
|
149
141
|
content_security_policy_nonce(request, CSP::STYLE_SRC)
|
|
150
142
|
end
|
|
151
143
|
|
|
144
|
+
# Public: Retreives the config for a given header type:
|
|
145
|
+
#
|
|
146
|
+
# Checks to see if there is an override for this request, then
|
|
147
|
+
# Checks to see if a named override is used for this request, then
|
|
148
|
+
# Falls back to the global config
|
|
149
|
+
def config_for(request)
|
|
150
|
+
config = request.env[SECURE_HEADERS_CONFIG] ||
|
|
151
|
+
Configuration.get(Configuration::DEFAULT_CONFIG)
|
|
152
|
+
|
|
153
|
+
if config.frozen?
|
|
154
|
+
config.dup
|
|
155
|
+
else
|
|
156
|
+
config
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
152
160
|
private
|
|
153
161
|
|
|
154
162
|
# Private: gets or creates a nonce for CSP.
|
|
@@ -181,64 +189,30 @@ module SecureHeaders
|
|
|
181
189
|
end
|
|
182
190
|
end
|
|
183
191
|
|
|
184
|
-
# Private: do the heavy lifting of converting a configuration object
|
|
185
|
-
# to a hash of headers valid for this request.
|
|
186
|
-
#
|
|
187
|
-
# Returns a hash of header names / values.
|
|
188
|
-
def build_headers(config, request)
|
|
189
|
-
header_classes_for(request).each_with_object({}) do |klass, hash|
|
|
190
|
-
header_config = if config
|
|
191
|
-
config.fetch(klass::CONFIG_KEY)
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
header_name, value = if klass == CSP
|
|
195
|
-
make_header(klass, header_config, request.user_agent)
|
|
196
|
-
else
|
|
197
|
-
make_header(klass, header_config)
|
|
198
|
-
end
|
|
199
|
-
hash[header_name] = value if value
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
|
|
203
192
|
# Private: takes a precomputed hash of headers and returns the Headers
|
|
204
193
|
# customized for the request.
|
|
205
194
|
#
|
|
206
195
|
# Returns a hash of header names / values valid for a given request.
|
|
207
|
-
def use_cached_headers(
|
|
196
|
+
def use_cached_headers(headers, request)
|
|
208
197
|
header_classes_for(request).each_with_object({}) do |klass, hash|
|
|
209
|
-
if
|
|
198
|
+
if header = headers[klass::CONFIG_KEY]
|
|
210
199
|
header_name, value = if klass == CSP
|
|
211
|
-
|
|
200
|
+
csp_header_for_ua(header, request)
|
|
212
201
|
else
|
|
213
|
-
|
|
202
|
+
header
|
|
214
203
|
end
|
|
215
204
|
hash[header_name] = value
|
|
216
205
|
end
|
|
217
206
|
end
|
|
218
207
|
end
|
|
219
208
|
|
|
220
|
-
# Private: Retreives the config for a given header type:
|
|
221
|
-
#
|
|
222
|
-
# Checks to see if there is an override for this request, then
|
|
223
|
-
# Checks to see if a named override is used for this request, then
|
|
224
|
-
# Falls back to the global config
|
|
225
|
-
def config_for(request)
|
|
226
|
-
request.env[SECURE_HEADERS_CONFIG] ||
|
|
227
|
-
Configuration.get(Configuration::DEFAULT_CONFIG)
|
|
228
|
-
end
|
|
229
|
-
|
|
230
209
|
# Private: chooses the applicable CSP header for the provided user agent.
|
|
231
210
|
#
|
|
232
211
|
# headers - a hash of header_config_key => [header_name, header_value]
|
|
233
212
|
#
|
|
234
213
|
# Returns a CSP [header, value] array
|
|
235
|
-
def
|
|
236
|
-
|
|
237
|
-
if CSP::VARIATIONS.key?(family)
|
|
238
|
-
headers[family]
|
|
239
|
-
else
|
|
240
|
-
headers[CSP::OTHER]
|
|
241
|
-
end
|
|
214
|
+
def csp_header_for_ua(headers, request)
|
|
215
|
+
headers[CSP.ua_to_variation(UserAgent.parse(request.user_agent))]
|
|
242
216
|
end
|
|
243
217
|
|
|
244
218
|
# Private: optionally build a header with a given configure
|
data/secure_headers.gemspec
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
2
|
Gem::Specification.new do |gem|
|
|
3
3
|
gem.name = "secure_headers"
|
|
4
|
-
gem.version = "3.
|
|
4
|
+
gem.version = "3.1.0"
|
|
5
5
|
gem.authors = ["Neil Matatall"]
|
|
6
6
|
gem.email = ["neil.matatall@gmail.com"]
|
|
7
7
|
gem.description = 'Security related headers all in one gem.'
|
|
@@ -29,16 +29,14 @@ module SecureHeaders
|
|
|
29
29
|
expect_default_values(header_hash)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
it "copies
|
|
32
|
+
it "copies config values when duping" do
|
|
33
33
|
Configuration.override(:test_override, Configuration::NOOP_CONFIGURATION) do
|
|
34
34
|
# do nothing, just copy it
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
config = Configuration.get(:test_override)
|
|
38
38
|
noop = Configuration.get(Configuration::NOOP_CONFIGURATION)
|
|
39
|
-
[:
|
|
40
|
-
:x_download_options, :x_permitted_cross_domain_policies, :hpkp, :csp].each do |key|
|
|
41
|
-
|
|
39
|
+
[:csp, :dynamic_csp, :secure_cookies].each do |key|
|
|
42
40
|
expect(config.send(key)).to eq(noop.send(key)), "Value not copied: #{key}."
|
|
43
41
|
end
|
|
44
42
|
end
|
|
@@ -22,142 +22,6 @@ module SecureHeaders
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
describe "#validate_config!" do
|
|
26
|
-
it "accepts all keys" do
|
|
27
|
-
# (pulled from README)
|
|
28
|
-
config = {
|
|
29
|
-
# "meta" values. these will shaped the header, but the values are not included in the header.
|
|
30
|
-
report_only: true, # default: false
|
|
31
|
-
preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
|
|
32
|
-
|
|
33
|
-
# directive values: these values will directly translate into source directives
|
|
34
|
-
default_src: %w(https: 'self'),
|
|
35
|
-
frame_src: %w('self' *.twimg.com itunes.apple.com),
|
|
36
|
-
connect_src: %w(wws:),
|
|
37
|
-
font_src: %w('self' data:),
|
|
38
|
-
img_src: %w(mycdn.com data:),
|
|
39
|
-
media_src: %w(utoob.com),
|
|
40
|
-
object_src: %w('self'),
|
|
41
|
-
script_src: %w('self'),
|
|
42
|
-
style_src: %w('unsafe-inline'),
|
|
43
|
-
base_uri: %w('self'),
|
|
44
|
-
child_src: %w('self'),
|
|
45
|
-
form_action: %w('self' github.com),
|
|
46
|
-
frame_ancestors: %w('none'),
|
|
47
|
-
plugin_types: %w(application/x-shockwave-flash),
|
|
48
|
-
block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
|
|
49
|
-
report_uri: %w(https://example.com/uri-directive)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
CSP.validate_config!(config)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
it "requires a :default_src value" do
|
|
56
|
-
expect do
|
|
57
|
-
CSP.validate_config!(script_src: %('self'))
|
|
58
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
it "requires :report_only to be a truthy value" do
|
|
62
|
-
expect do
|
|
63
|
-
CSP.validate_config!(default_opts.merge(report_only: "steve"))
|
|
64
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
it "requires :preserve_schemes to be a truthy value" do
|
|
68
|
-
expect do
|
|
69
|
-
CSP.validate_config!(default_opts.merge(preserve_schemes: "steve"))
|
|
70
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
it "requires :block_all_mixed_content to be a boolean value" do
|
|
74
|
-
expect do
|
|
75
|
-
CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve"))
|
|
76
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
it "requires all source lists to be an array of strings" do
|
|
80
|
-
expect do
|
|
81
|
-
CSP.validate_config!(default_src: "steve")
|
|
82
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
it "allows nil values" do
|
|
86
|
-
expect do
|
|
87
|
-
CSP.validate_config!(default_src: %w('self'), script_src: ["https:", nil])
|
|
88
|
-
end.to_not raise_error
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
it "rejects unknown directives / config" do
|
|
92
|
-
expect do
|
|
93
|
-
CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve")
|
|
94
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# this is mostly to ensure people don't use the antiquated shorthands common in other configs
|
|
98
|
-
it "performs light validation on source lists" do
|
|
99
|
-
expect do
|
|
100
|
-
CSP.validate_config!(default_src: %w(self none inline eval))
|
|
101
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
describe "#combine_policies" do
|
|
106
|
-
it "combines the default-src value with the override if the directive was unconfigured" do
|
|
107
|
-
combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com))
|
|
108
|
-
csp = ContentSecurityPolicy.new(combined_config)
|
|
109
|
-
expect(csp.name).to eq(CSP::HEADER_NAME)
|
|
110
|
-
expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com")
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
it "overrides the report_only flag" do
|
|
114
|
-
Configuration.default do |config|
|
|
115
|
-
config.csp = {
|
|
116
|
-
default_src: %w('self'),
|
|
117
|
-
report_only: false
|
|
118
|
-
}
|
|
119
|
-
end
|
|
120
|
-
combined_config = CSP.combine_policies(Configuration.get.csp, report_only: true)
|
|
121
|
-
csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
|
|
122
|
-
expect(csp.name).to eq(CSP::REPORT_ONLY)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
it "overrides the :block_all_mixed_content flag" do
|
|
126
|
-
Configuration.default do |config|
|
|
127
|
-
config.csp = {
|
|
128
|
-
default_src: %w(https:),
|
|
129
|
-
block_all_mixed_content: false
|
|
130
|
-
}
|
|
131
|
-
end
|
|
132
|
-
combined_config = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true)
|
|
133
|
-
csp = ContentSecurityPolicy.new(combined_config)
|
|
134
|
-
expect(csp.value).to eq("default-src https:; block-all-mixed-content")
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
it "raises an error if appending to a OPT_OUT policy" do
|
|
138
|
-
Configuration.default do |config|
|
|
139
|
-
config.csp = OPT_OUT
|
|
140
|
-
end
|
|
141
|
-
expect do
|
|
142
|
-
CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com))
|
|
143
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
describe "#idempotent_additions?" do
|
|
148
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false }
|
|
149
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false }
|
|
150
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false }
|
|
151
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false }
|
|
152
|
-
|
|
153
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true }
|
|
154
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true }
|
|
155
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true }
|
|
156
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true }
|
|
157
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true }
|
|
158
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true }
|
|
159
|
-
end
|
|
160
|
-
|
|
161
25
|
describe "#value" do
|
|
162
26
|
it "discards 'none' values if any other source expressions are present" do
|
|
163
27
|
csp = ContentSecurityPolicy.new(default_opts.merge(frame_src: %w('self' 'none')))
|
|
@@ -214,27 +78,33 @@ module SecureHeaders
|
|
|
214
78
|
|
|
215
79
|
context "browser sniffing" do
|
|
216
80
|
let (:complex_opts) do
|
|
217
|
-
ContentSecurityPolicy::ALL_DIRECTIVES.each_with_object({})
|
|
218
|
-
|
|
219
|
-
|
|
81
|
+
ContentSecurityPolicy::ALL_DIRECTIVES.each_with_object({}) do |directive, hash|
|
|
82
|
+
hash[directive] = %w('self')
|
|
83
|
+
end.merge({
|
|
84
|
+
block_all_mixed_content: true,
|
|
85
|
+
upgrade_insecure_requests: true,
|
|
86
|
+
reflected_xss: "block",
|
|
87
|
+
script_src: %w('self'),
|
|
88
|
+
script_nonce: 123456
|
|
89
|
+
})
|
|
220
90
|
end
|
|
221
91
|
|
|
222
92
|
it "does not filter any directives for Chrome" do
|
|
223
93
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
|
|
224
|
-
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'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; report-uri 'self'")
|
|
94
|
+
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'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'")
|
|
225
95
|
end
|
|
226
96
|
|
|
227
97
|
it "does not filter any directives for Opera" do
|
|
228
98
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
|
|
229
|
-
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'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; report-uri 'self'")
|
|
99
|
+
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'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'")
|
|
230
100
|
end
|
|
231
101
|
|
|
232
102
|
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
|
|
233
103
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
|
|
234
|
-
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'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; report-uri 'self'")
|
|
104
|
+
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'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; upgrade-insecure-requests; report-uri 'self'")
|
|
235
105
|
end
|
|
236
106
|
|
|
237
|
-
it "adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do
|
|
107
|
+
it "adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do
|
|
238
108
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
|
|
239
109
|
expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; report-uri 'self'")
|
|
240
110
|
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module SecureHeaders
|
|
4
|
+
describe PolicyManagement do
|
|
5
|
+
let (:default_opts) do
|
|
6
|
+
{
|
|
7
|
+
default_src: %w(https:),
|
|
8
|
+
img_src: %w(https: data:),
|
|
9
|
+
script_src: %w('unsafe-inline' 'unsafe-eval' https: data:),
|
|
10
|
+
style_src: %w('unsafe-inline' https: about:),
|
|
11
|
+
report_uri: %w(/csp_report)
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe "#validate_config!" do
|
|
16
|
+
it "accepts all keys" do
|
|
17
|
+
# (pulled from README)
|
|
18
|
+
config = {
|
|
19
|
+
# "meta" values. these will shaped the header, but the values are not included in the header.
|
|
20
|
+
report_only: true, # default: false
|
|
21
|
+
preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
|
|
22
|
+
|
|
23
|
+
# directive values: these values will directly translate into source directives
|
|
24
|
+
default_src: %w(https: 'self'),
|
|
25
|
+
frame_src: %w('self' *.twimg.com itunes.apple.com),
|
|
26
|
+
connect_src: %w(wws:),
|
|
27
|
+
font_src: %w('self' data:),
|
|
28
|
+
img_src: %w(mycdn.com data:),
|
|
29
|
+
media_src: %w(utoob.com),
|
|
30
|
+
object_src: %w('self'),
|
|
31
|
+
script_src: %w('self'),
|
|
32
|
+
style_src: %w('unsafe-inline'),
|
|
33
|
+
base_uri: %w('self'),
|
|
34
|
+
child_src: %w('self'),
|
|
35
|
+
form_action: %w('self' github.com),
|
|
36
|
+
frame_ancestors: %w('none'),
|
|
37
|
+
plugin_types: %w(application/x-shockwave-flash),
|
|
38
|
+
block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
|
|
39
|
+
upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
|
|
40
|
+
report_uri: %w(https://example.com/uri-directive)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
CSP.validate_config!(config)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "requires a :default_src value" do
|
|
47
|
+
expect do
|
|
48
|
+
CSP.validate_config!(script_src: %('self'))
|
|
49
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "requires :report_only to be a truthy value" do
|
|
53
|
+
expect do
|
|
54
|
+
CSP.validate_config!(default_opts.merge(report_only: "steve"))
|
|
55
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "requires :preserve_schemes to be a truthy value" do
|
|
59
|
+
expect do
|
|
60
|
+
CSP.validate_config!(default_opts.merge(preserve_schemes: "steve"))
|
|
61
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "requires :block_all_mixed_content to be a boolean value" do
|
|
65
|
+
expect do
|
|
66
|
+
CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve"))
|
|
67
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "requires :upgrade_insecure_requests to be a boolean value" do
|
|
71
|
+
expect do
|
|
72
|
+
CSP.validate_config!(default_opts.merge(upgrade_insecure_requests: "steve"))
|
|
73
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "requires all source lists to be an array of strings" do
|
|
77
|
+
expect do
|
|
78
|
+
CSP.validate_config!(default_src: "steve")
|
|
79
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "allows nil values" do
|
|
83
|
+
expect do
|
|
84
|
+
CSP.validate_config!(default_src: %w('self'), script_src: ["https:", nil])
|
|
85
|
+
end.to_not raise_error
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "rejects unknown directives / config" do
|
|
89
|
+
expect do
|
|
90
|
+
CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve")
|
|
91
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# this is mostly to ensure people don't use the antiquated shorthands common in other configs
|
|
95
|
+
it "performs light validation on source lists" do
|
|
96
|
+
expect do
|
|
97
|
+
CSP.validate_config!(default_src: %w(self none inline eval))
|
|
98
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
describe "#combine_policies" do
|
|
103
|
+
it "combines the default-src value with the override if the directive was unconfigured" do
|
|
104
|
+
combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com))
|
|
105
|
+
csp = ContentSecurityPolicy.new(combined_config)
|
|
106
|
+
expect(csp.name).to eq(CSP::HEADER_NAME)
|
|
107
|
+
expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "combines directives where the original value is nil and the hash is frozen" do
|
|
111
|
+
Configuration.default do |config|
|
|
112
|
+
config.csp = {
|
|
113
|
+
default_src: %w('self'),
|
|
114
|
+
report_only: false
|
|
115
|
+
}.freeze
|
|
116
|
+
end
|
|
117
|
+
report_uri = "https://report-uri.io/asdf"
|
|
118
|
+
combined_config = CSP.combine_policies(Configuration.get.csp, report_uri: [report_uri])
|
|
119
|
+
csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
|
|
120
|
+
expect(csp.value).to include("report-uri #{report_uri}")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "does not combine the default-src value for directives that don't fall back to default sources" do
|
|
124
|
+
Configuration.default do |config|
|
|
125
|
+
config.csp = {
|
|
126
|
+
default_src: %w('self'),
|
|
127
|
+
report_only: false
|
|
128
|
+
}.freeze
|
|
129
|
+
end
|
|
130
|
+
non_default_source_additions = CSP::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash|
|
|
131
|
+
hash[directive] = %w("http://example.org)
|
|
132
|
+
end
|
|
133
|
+
combined_config = CSP.combine_policies(Configuration.get.csp, non_default_source_additions)
|
|
134
|
+
|
|
135
|
+
CSP::NON_FETCH_SOURCES.each do |directive|
|
|
136
|
+
expect(combined_config[directive]).to eq(%w("http://example.org))
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]).value
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "overrides the report_only flag" do
|
|
143
|
+
Configuration.default do |config|
|
|
144
|
+
config.csp = {
|
|
145
|
+
default_src: %w('self'),
|
|
146
|
+
report_only: false
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
combined_config = CSP.combine_policies(Configuration.get.csp, report_only: true)
|
|
150
|
+
csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
|
|
151
|
+
expect(csp.name).to eq(CSP::REPORT_ONLY)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "overrides the :block_all_mixed_content flag" do
|
|
155
|
+
Configuration.default do |config|
|
|
156
|
+
config.csp = {
|
|
157
|
+
default_src: %w(https:),
|
|
158
|
+
block_all_mixed_content: false
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
combined_config = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true)
|
|
162
|
+
csp = ContentSecurityPolicy.new(combined_config)
|
|
163
|
+
expect(csp.value).to eq("default-src https:; block-all-mixed-content")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it "raises an error if appending to a OPT_OUT policy" do
|
|
167
|
+
Configuration.default do |config|
|
|
168
|
+
config.csp = OPT_OUT
|
|
169
|
+
end
|
|
170
|
+
expect do
|
|
171
|
+
CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com))
|
|
172
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
describe "#idempotent_additions?" do
|
|
177
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false }
|
|
178
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false }
|
|
179
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false }
|
|
180
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false }
|
|
181
|
+
|
|
182
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true }
|
|
183
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true }
|
|
184
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true }
|
|
185
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true }
|
|
186
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true }
|
|
187
|
+
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true }
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -4,7 +4,7 @@ module SecureHeaders
|
|
|
4
4
|
describe StrictTransportSecurity do
|
|
5
5
|
describe "#value" do
|
|
6
6
|
specify { expect(StrictTransportSecurity.make_header).to eq([StrictTransportSecurity::HEADER_NAME, StrictTransportSecurity::DEFAULT_VALUE]) }
|
|
7
|
-
specify { expect(StrictTransportSecurity.make_header("max-age=1234")).to eq([StrictTransportSecurity::HEADER_NAME, "max-age=1234"]) }
|
|
7
|
+
specify { expect(StrictTransportSecurity.make_header("max-age=1234; includeSubdomains; preload")).to eq([StrictTransportSecurity::HEADER_NAME, "max-age=1234; includeSubdomains; preload"]) }
|
|
8
8
|
|
|
9
9
|
context "with an invalid configuration" do
|
|
10
10
|
context "with a string argument" do
|