secure_headers 3.0.3 → 3.2.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 +144 -0
- data/Gemfile +5 -2
- data/README.md +152 -61
- data/lib/secure_headers/configuration.rb +109 -54
- data/lib/secure_headers/hash_helper.rb +10 -0
- data/lib/secure_headers/headers/content_security_policy.rb +31 -309
- data/lib/secure_headers/headers/cookie.rb +126 -0
- data/lib/secure_headers/headers/policy_management.rb +320 -0
- data/lib/secure_headers/headers/x_content_type_options.rb +1 -1
- data/lib/secure_headers/middleware.rb +37 -0
- data/lib/secure_headers/railtie.rb +5 -1
- data/lib/secure_headers/utils/cookies_config.rb +94 -0
- data/lib/secure_headers/view_helper.rb +64 -0
- data/lib/secure_headers.rb +39 -63
- data/lib/tasks/tasks.rake +81 -0
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/configuration_spec.rb +18 -4
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +0 -175
- data/spec/lib/secure_headers/headers/cookie_spec.rb +164 -0
- 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 +59 -7
- data/spec/lib/secure_headers/view_helpers_spec.rb +125 -0
- data/spec/lib/secure_headers_spec.rb +119 -42
- data/spec/spec_helper.rb +16 -1
- data/upgrading-to-3-0.md +1 -0
- metadata +13 -4
- data/lib/secure_headers/padrino.rb +0 -13
- data/travis.sh +0 -10
data/lib/secure_headers.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require "secure_headers/configuration"
|
|
2
|
+
require "secure_headers/hash_helper"
|
|
3
|
+
require "secure_headers/headers/cookie"
|
|
2
4
|
require "secure_headers/headers/public_key_pins"
|
|
3
5
|
require "secure_headers/headers/content_security_policy"
|
|
4
6
|
require "secure_headers/headers/x_frame_options"
|
|
@@ -48,14 +50,12 @@ module SecureHeaders
|
|
|
48
50
|
# script_src: %w(another-host.com)
|
|
49
51
|
def override_content_security_policy_directives(request, additions)
|
|
50
52
|
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)
|
|
53
|
+
if config.current_csp == OPT_OUT
|
|
54
|
+
config.dynamic_csp = {}
|
|
58
55
|
end
|
|
56
|
+
|
|
57
|
+
config.dynamic_csp = config.current_csp.merge(additions)
|
|
58
|
+
override_secure_headers_request_config(request, config)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
# Public: appends source values to the current configuration. If no value
|
|
@@ -66,11 +66,8 @@ module SecureHeaders
|
|
|
66
66
|
# script_src: %w(another-host.com)
|
|
67
67
|
def append_content_security_policy_directives(request, additions)
|
|
68
68
|
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
|
|
69
|
+
config.dynamic_csp = CSP.combine_policies(config.current_csp, additions)
|
|
70
|
+
override_secure_headers_request_config(request, config)
|
|
74
71
|
end
|
|
75
72
|
|
|
76
73
|
# Public: override X-Frame-Options settings for this request.
|
|
@@ -79,16 +76,16 @@ module SecureHeaders
|
|
|
79
76
|
#
|
|
80
77
|
# Returns the current config
|
|
81
78
|
def override_x_frame_options(request, value)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
override_secure_headers_request_config(request,
|
|
79
|
+
config = config_for(request)
|
|
80
|
+
config.update_x_frame_options(value)
|
|
81
|
+
override_secure_headers_request_config(request, config)
|
|
85
82
|
end
|
|
86
83
|
|
|
87
84
|
# Public: opts out of setting a given header by creating a temporary config
|
|
88
85
|
# and setting the given headers config to OPT_OUT.
|
|
89
86
|
def opt_out_of_header(request, header_key)
|
|
90
|
-
config = config_for(request)
|
|
91
|
-
config.
|
|
87
|
+
config = config_for(request)
|
|
88
|
+
config.opt_out(header_key)
|
|
92
89
|
override_secure_headers_request_config(request, config)
|
|
93
90
|
end
|
|
94
91
|
|
|
@@ -109,14 +106,11 @@ module SecureHeaders
|
|
|
109
106
|
# in Rack middleware.
|
|
110
107
|
def header_hash_for(request)
|
|
111
108
|
config = config_for(request)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
use_cached_headers(cached_headers, request)
|
|
115
|
-
else
|
|
116
|
-
build_headers(config, request)
|
|
109
|
+
unless ContentSecurityPolicy.idempotent_additions?(config.csp, config.current_csp)
|
|
110
|
+
config.rebuild_csp_header_cache!(request.user_agent)
|
|
117
111
|
end
|
|
118
112
|
|
|
119
|
-
|
|
113
|
+
use_cached_headers(config.cached_headers, request)
|
|
120
114
|
end
|
|
121
115
|
|
|
122
116
|
# Public: specify which named override will be used for this request.
|
|
@@ -149,6 +143,22 @@ module SecureHeaders
|
|
|
149
143
|
content_security_policy_nonce(request, CSP::STYLE_SRC)
|
|
150
144
|
end
|
|
151
145
|
|
|
146
|
+
# Public: Retreives the config for a given header type:
|
|
147
|
+
#
|
|
148
|
+
# Checks to see if there is an override for this request, then
|
|
149
|
+
# Checks to see if a named override is used for this request, then
|
|
150
|
+
# Falls back to the global config
|
|
151
|
+
def config_for(request)
|
|
152
|
+
config = request.env[SECURE_HEADERS_CONFIG] ||
|
|
153
|
+
Configuration.get(Configuration::DEFAULT_CONFIG)
|
|
154
|
+
|
|
155
|
+
if config.frozen?
|
|
156
|
+
config.dup
|
|
157
|
+
else
|
|
158
|
+
config
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
152
162
|
private
|
|
153
163
|
|
|
154
164
|
# Private: gets or creates a nonce for CSP.
|
|
@@ -181,64 +191,30 @@ module SecureHeaders
|
|
|
181
191
|
end
|
|
182
192
|
end
|
|
183
193
|
|
|
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
194
|
# Private: takes a precomputed hash of headers and returns the Headers
|
|
204
195
|
# customized for the request.
|
|
205
196
|
#
|
|
206
197
|
# Returns a hash of header names / values valid for a given request.
|
|
207
|
-
def use_cached_headers(
|
|
198
|
+
def use_cached_headers(headers, request)
|
|
208
199
|
header_classes_for(request).each_with_object({}) do |klass, hash|
|
|
209
|
-
if
|
|
200
|
+
if header = headers[klass::CONFIG_KEY]
|
|
210
201
|
header_name, value = if klass == CSP
|
|
211
|
-
|
|
202
|
+
csp_header_for_ua(header, request)
|
|
212
203
|
else
|
|
213
|
-
|
|
204
|
+
header
|
|
214
205
|
end
|
|
215
206
|
hash[header_name] = value
|
|
216
207
|
end
|
|
217
208
|
end
|
|
218
209
|
end
|
|
219
210
|
|
|
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
211
|
# Private: chooses the applicable CSP header for the provided user agent.
|
|
231
212
|
#
|
|
232
213
|
# headers - a hash of header_config_key => [header_name, header_value]
|
|
233
214
|
#
|
|
234
215
|
# 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
|
|
216
|
+
def csp_header_for_ua(headers, request)
|
|
217
|
+
headers[CSP.ua_to_variation(UserAgent.parse(request.user_agent))]
|
|
242
218
|
end
|
|
243
219
|
|
|
244
220
|
# Private: optionally build a header with a given configure
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx
|
|
2
|
+
INLINE_STYLE_REGEX = /(<style[^>]*>)(.*?)<\/style>/mx
|
|
3
|
+
INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
|
|
4
|
+
INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
|
|
5
|
+
|
|
6
|
+
namespace :secure_headers do
|
|
7
|
+
include SecureHeaders::HashHelper
|
|
8
|
+
|
|
9
|
+
def is_erb?(filename)
|
|
10
|
+
filename =~ /\.erb\Z/
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def is_mustache?(filename)
|
|
14
|
+
filename =~ /\.mustache\Z/
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def dynamic_content?(filename, inline_script)
|
|
18
|
+
(is_mustache?(filename) && inline_script =~ /\{\{.*\}\}/) ||
|
|
19
|
+
(is_erb?(filename) && inline_script =~ /<%.*%>/)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def find_inline_content(filename, regex, hashes)
|
|
23
|
+
file = File.read(filename)
|
|
24
|
+
file.scan(regex) do # TODO don't use gsub
|
|
25
|
+
inline_script = Regexp.last_match.captures.last
|
|
26
|
+
if dynamic_content?(filename, inline_script)
|
|
27
|
+
puts "Looks like there's some dynamic content inside of a tag :-/"
|
|
28
|
+
puts "That pretty much means the hash value will never match."
|
|
29
|
+
puts "Code: " + inline_script
|
|
30
|
+
puts "=" * 20
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
hashes << hash_source(inline_script)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def generate_inline_script_hashes(filename)
|
|
38
|
+
hashes = []
|
|
39
|
+
|
|
40
|
+
[INLINE_SCRIPT_REGEX, INLINE_HASH_SCRIPT_HELPER_REGEX].each do |regex|
|
|
41
|
+
find_inline_content(filename, regex, hashes)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
hashes
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def generate_inline_style_hashes(filename)
|
|
48
|
+
hashes = []
|
|
49
|
+
|
|
50
|
+
[INLINE_STYLE_REGEX, INLINE_HASH_STYLE_HELPER_REGEX].each do |regex|
|
|
51
|
+
find_inline_content(filename, regex, hashes)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
hashes
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
task :generate_hashes do |t, args|
|
|
58
|
+
script_hashes = {
|
|
59
|
+
"scripts" => {},
|
|
60
|
+
"styles" => {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Dir.glob("app/{views,templates}/**/*.{erb,mustache}") do |filename|
|
|
64
|
+
hashes = generate_inline_script_hashes(filename)
|
|
65
|
+
if hashes.any?
|
|
66
|
+
script_hashes["scripts"][filename] = hashes
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
hashes = generate_inline_style_hashes(filename)
|
|
70
|
+
if hashes.any?
|
|
71
|
+
script_hashes["styles"][filename] = hashes
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE, 'w') do |file|
|
|
76
|
+
file.write(script_hashes.to_yaml)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
puts "Script hashes from " + script_hashes.keys.size.to_s + " files added to #{SecureHeaders::Configuration::HASH_CONFIG_FILE}"
|
|
80
|
+
end
|
|
81
|
+
end
|
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.0
|
|
4
|
+
gem.version = "3.2.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,20 +29,26 @@ 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, :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
|
|
45
43
|
|
|
44
|
+
it "regenerates cached headers when building an override" do
|
|
45
|
+
Configuration.override(:test_override) do |config|
|
|
46
|
+
config.x_content_type_options = OPT_OUT
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
expect(Configuration.get.cached_headers).to_not eq(Configuration.get(:test_override).cached_headers)
|
|
50
|
+
end
|
|
51
|
+
|
|
46
52
|
it "stores an override of the global config" do
|
|
47
53
|
Configuration.override(:test_override) do |config|
|
|
48
54
|
config.x_frame_options = "DENY"
|
|
@@ -76,5 +82,13 @@ module SecureHeaders
|
|
|
76
82
|
override_config = Configuration.get(:second_override)
|
|
77
83
|
expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org))
|
|
78
84
|
end
|
|
85
|
+
|
|
86
|
+
it "deprecates the secure_cookies configuration" do
|
|
87
|
+
expect(Kernel).to receive(:warn).with(/\[DEPRECATION\]/)
|
|
88
|
+
|
|
89
|
+
Configuration.default do |config|
|
|
90
|
+
config.secure_cookies = true
|
|
91
|
+
end
|
|
92
|
+
end
|
|
79
93
|
end
|
|
80
94
|
end
|
|
@@ -22,181 +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
|
-
upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
|
|
50
|
-
report_uri: %w(https://example.com/uri-directive)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
CSP.validate_config!(config)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
it "requires a :default_src value" do
|
|
57
|
-
expect do
|
|
58
|
-
CSP.validate_config!(script_src: %('self'))
|
|
59
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
it "requires :report_only to be a truthy value" do
|
|
63
|
-
expect do
|
|
64
|
-
CSP.validate_config!(default_opts.merge(report_only: "steve"))
|
|
65
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
it "requires :preserve_schemes to be a truthy value" do
|
|
69
|
-
expect do
|
|
70
|
-
CSP.validate_config!(default_opts.merge(preserve_schemes: "steve"))
|
|
71
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
it "requires :block_all_mixed_content to be a boolean value" do
|
|
75
|
-
expect do
|
|
76
|
-
CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve"))
|
|
77
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
it "requires :upgrade_insecure_requests to be a boolean value" do
|
|
81
|
-
expect do
|
|
82
|
-
CSP.validate_config!(default_opts.merge(upgrade_insecure_requests: "steve"))
|
|
83
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
it "requires all source lists to be an array of strings" do
|
|
87
|
-
expect do
|
|
88
|
-
CSP.validate_config!(default_src: "steve")
|
|
89
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
it "allows nil values" do
|
|
93
|
-
expect do
|
|
94
|
-
CSP.validate_config!(default_src: %w('self'), script_src: ["https:", nil])
|
|
95
|
-
end.to_not raise_error
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
it "rejects unknown directives / config" do
|
|
99
|
-
expect do
|
|
100
|
-
CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve")
|
|
101
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# this is mostly to ensure people don't use the antiquated shorthands common in other configs
|
|
105
|
-
it "performs light validation on source lists" do
|
|
106
|
-
expect do
|
|
107
|
-
CSP.validate_config!(default_src: %w(self none inline eval))
|
|
108
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
describe "#combine_policies" do
|
|
113
|
-
it "combines the default-src value with the override if the directive was unconfigured" do
|
|
114
|
-
combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com))
|
|
115
|
-
csp = ContentSecurityPolicy.new(combined_config)
|
|
116
|
-
expect(csp.name).to eq(CSP::HEADER_NAME)
|
|
117
|
-
expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com")
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
it "combines directives where the original value is nil and the hash is frozen" do
|
|
121
|
-
Configuration.default do |config|
|
|
122
|
-
config.csp = {
|
|
123
|
-
default_src: %w('self'),
|
|
124
|
-
report_only: false
|
|
125
|
-
}.freeze
|
|
126
|
-
end
|
|
127
|
-
report_uri = "https://report-uri.io/asdf"
|
|
128
|
-
combined_config = CSP.combine_policies(Configuration.get.csp, report_uri: [report_uri])
|
|
129
|
-
csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
|
|
130
|
-
expect(csp.value).to include("report-uri #{report_uri}")
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
it "does not combine the default-src value for directives that don't fall back to default sources" do
|
|
134
|
-
Configuration.default do |config|
|
|
135
|
-
config.csp = {
|
|
136
|
-
default_src: %w('self'),
|
|
137
|
-
report_only: false
|
|
138
|
-
}.freeze
|
|
139
|
-
end
|
|
140
|
-
non_default_source_additions = CSP::NON_DEFAULT_SOURCES.each_with_object({}) do |directive, hash|
|
|
141
|
-
hash[directive] = %w("http://example.org)
|
|
142
|
-
end
|
|
143
|
-
combined_config = CSP.combine_policies(Configuration.get.csp, non_default_source_additions)
|
|
144
|
-
|
|
145
|
-
CSP::NON_DEFAULT_SOURCES.each do |directive|
|
|
146
|
-
expect(combined_config[directive]).to eq(%w("http://example.org))
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]).value
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
it "overrides the report_only flag" do
|
|
153
|
-
Configuration.default do |config|
|
|
154
|
-
config.csp = {
|
|
155
|
-
default_src: %w('self'),
|
|
156
|
-
report_only: false
|
|
157
|
-
}
|
|
158
|
-
end
|
|
159
|
-
combined_config = CSP.combine_policies(Configuration.get.csp, report_only: true)
|
|
160
|
-
csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
|
|
161
|
-
expect(csp.name).to eq(CSP::REPORT_ONLY)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
it "overrides the :block_all_mixed_content flag" do
|
|
165
|
-
Configuration.default do |config|
|
|
166
|
-
config.csp = {
|
|
167
|
-
default_src: %w(https:),
|
|
168
|
-
block_all_mixed_content: false
|
|
169
|
-
}
|
|
170
|
-
end
|
|
171
|
-
combined_config = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true)
|
|
172
|
-
csp = ContentSecurityPolicy.new(combined_config)
|
|
173
|
-
expect(csp.value).to eq("default-src https:; block-all-mixed-content")
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
it "raises an error if appending to a OPT_OUT policy" do
|
|
177
|
-
Configuration.default do |config|
|
|
178
|
-
config.csp = OPT_OUT
|
|
179
|
-
end
|
|
180
|
-
expect do
|
|
181
|
-
CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com))
|
|
182
|
-
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
describe "#idempotent_additions?" do
|
|
187
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false }
|
|
188
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false }
|
|
189
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false }
|
|
190
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false }
|
|
191
|
-
|
|
192
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true }
|
|
193
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true }
|
|
194
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true }
|
|
195
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true }
|
|
196
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true }
|
|
197
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true }
|
|
198
|
-
end
|
|
199
|
-
|
|
200
25
|
describe "#value" do
|
|
201
26
|
it "discards 'none' values if any other source expressions are present" do
|
|
202
27
|
csp = ContentSecurityPolicy.new(default_opts.merge(frame_src: %w('self' 'none')))
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module SecureHeaders
|
|
4
|
+
describe Cookie do
|
|
5
|
+
let(:raw_cookie) { "_session=thisisatest" }
|
|
6
|
+
|
|
7
|
+
it "does not tamper with cookies when unconfigured" do
|
|
8
|
+
cookie = Cookie.new(raw_cookie, {})
|
|
9
|
+
expect(cookie.to_s).to eq(raw_cookie)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "preserves existing attributes" do
|
|
13
|
+
cookie = Cookie.new("_session=thisisatest; secure", secure: true)
|
|
14
|
+
expect(cookie.to_s).to eq("_session=thisisatest; secure")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "prevents duplicate flagging of attributes" do
|
|
18
|
+
cookie = Cookie.new("_session=thisisatest; secure", secure: true)
|
|
19
|
+
expect(cookie.to_s.scan(/secure/i).count).to eq(1)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
context "Secure cookies" do
|
|
23
|
+
context "when configured with a boolean" do
|
|
24
|
+
it "flags cookies as Secure" do
|
|
25
|
+
cookie = Cookie.new(raw_cookie, secure: true)
|
|
26
|
+
expect(cookie.to_s).to eq("_session=thisisatest; secure")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context "when configured with a Hash" do
|
|
31
|
+
it "flags cookies as Secure when whitelisted" do
|
|
32
|
+
cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]})
|
|
33
|
+
expect(cookie.to_s).to eq("_session=thisisatest; secure")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "does not flag cookies as Secure when excluded" do
|
|
37
|
+
cookie = Cookie.new(raw_cookie, secure: { except: ["_session"] })
|
|
38
|
+
expect(cookie.to_s).to eq("_session=thisisatest")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
context "HttpOnly cookies" do
|
|
44
|
+
context "when configured with a boolean" do
|
|
45
|
+
it "flags cookies as HttpOnly" do
|
|
46
|
+
cookie = Cookie.new(raw_cookie, httponly: true)
|
|
47
|
+
expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context "when configured with a Hash" do
|
|
52
|
+
it "flags cookies as HttpOnly when whitelisted" do
|
|
53
|
+
cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]})
|
|
54
|
+
expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "does not flag cookies as HttpOnly when excluded" do
|
|
58
|
+
cookie = Cookie.new(raw_cookie, httponly: { except: ["_session"] })
|
|
59
|
+
expect(cookie.to_s).to eq("_session=thisisatest")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
context "SameSite cookies" do
|
|
65
|
+
it "flags SameSite=Lax" do
|
|
66
|
+
cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } })
|
|
67
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "flags SameSite=Lax when configured with a boolean" do
|
|
71
|
+
cookie = Cookie.new(raw_cookie, samesite: { lax: true})
|
|
72
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "does not flag cookies as SameSite=Lax when excluded" do
|
|
76
|
+
cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } })
|
|
77
|
+
expect(cookie.to_s).to eq("_session=thisisatest")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "flags SameSite=Strict" do
|
|
81
|
+
cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } })
|
|
82
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "does not flag cookies as SameSite=Strict when excluded" do
|
|
86
|
+
cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] } })
|
|
87
|
+
expect(cookie.to_s).to eq("_session=thisisatest")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "flags SameSite=Strict when configured with a boolean" do
|
|
91
|
+
cookie = Cookie.new(raw_cookie, samesite: { strict: true})
|
|
92
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "flags properly when both lax and strict are configured" do
|
|
96
|
+
raw_cookie = "_session=thisisatest"
|
|
97
|
+
cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } })
|
|
98
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "ignores configuration if the cookie is already flagged" do
|
|
102
|
+
raw_cookie = "_session=thisisatest; SameSite=Strict"
|
|
103
|
+
cookie = Cookie.new(raw_cookie, samesite: { lax: true })
|
|
104
|
+
expect(cookie.to_s).to eq(raw_cookie)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
context "with an invalid configuration" do
|
|
110
|
+
it "raises an exception when not configured with a Hash" do
|
|
111
|
+
expect do
|
|
112
|
+
Cookie.validate_config!("configuration")
|
|
113
|
+
end.to raise_error(CookiesConfigError)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "raises an exception when configured without a boolean/Hash" do
|
|
117
|
+
expect do
|
|
118
|
+
Cookie.validate_config!(secure: "true")
|
|
119
|
+
end.to raise_error(CookiesConfigError)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "raises an exception when both only and except filters are provided" do
|
|
123
|
+
expect do
|
|
124
|
+
Cookie.validate_config!(secure: { only: [], except: [] })
|
|
125
|
+
end.to raise_error(CookiesConfigError)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "raises an exception when SameSite is not configured with a Hash" do
|
|
129
|
+
expect do
|
|
130
|
+
Cookie.validate_config!(samesite: true)
|
|
131
|
+
end.to raise_error(CookiesConfigError)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do
|
|
135
|
+
expect do
|
|
136
|
+
Cookie.validate_config!(samesite: { lax: true, strict: true})
|
|
137
|
+
end.to raise_error(CookiesConfigError)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do
|
|
141
|
+
expect do
|
|
142
|
+
Cookie.validate_config!(samesite: { lax: true, strict: { only: ["_anything"] } })
|
|
143
|
+
end.to raise_error(CookiesConfigError)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it "raises an exception when both only and except filters are provided to SameSite configurations" do
|
|
147
|
+
expect do
|
|
148
|
+
Cookie.validate_config!(samesite: { lax: { only: ["_anything"], except: ["_anythingelse"] } })
|
|
149
|
+
end.to raise_error(CookiesConfigError)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "raises an exception when both lax and strict only filters are provided to SameSite configurations" do
|
|
153
|
+
expect do
|
|
154
|
+
Cookie.validate_config!(samesite: { lax: { only: ["_anything"] }, strict: { only: ["_anything"] } })
|
|
155
|
+
end.to raise_error(CookiesConfigError)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "raises an exception when both lax and strict only filters are provided to SameSite configurations" do
|
|
159
|
+
expect do
|
|
160
|
+
Cookie.validate_config!(samesite: { lax: { except: ["_anything"] }, strict: { except: ["_anything"] } })
|
|
161
|
+
end.to raise_error(CookiesConfigError)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|