secure_headers 3.0.3 → 4.0.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/.github/ISSUE_TEMPLATE.md +41 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- data/.gitignore +0 -9
- data/.rspec +2 -0
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -1
- data/.travis.yml +17 -5
- data/CHANGELOG.md +287 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +13 -5
- data/Guardfile +1 -0
- data/LICENSE +4 -199
- data/README.md +85 -265
- data/Rakefile +22 -18
- data/docs/HPKP.md +17 -0
- data/docs/cookies.md +64 -0
- data/docs/hashes.md +64 -0
- data/docs/named_overrides_and_appends.md +107 -0
- data/docs/per_action_configuration.md +133 -0
- data/docs/sinatra.md +25 -0
- data/lib/secure_headers/configuration.rb +188 -59
- data/lib/secure_headers/hash_helper.rb +11 -0
- data/lib/secure_headers/headers/clear_site_data.rb +55 -0
- data/lib/secure_headers/headers/content_security_policy.rb +136 -325
- data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
- data/lib/secure_headers/headers/cookie.rb +144 -0
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
- data/lib/secure_headers/headers/policy_management.rb +410 -0
- data/lib/secure_headers/headers/public_key_pins.rb +5 -4
- data/lib/secure_headers/headers/referrer_policy.rb +37 -0
- data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
- data/lib/secure_headers/headers/x_content_type_options.rb +3 -2
- data/lib/secure_headers/headers/x_download_options.rb +3 -2
- data/lib/secure_headers/headers/x_frame_options.rb +2 -1
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
- data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
- data/lib/secure_headers/middleware.rb +44 -0
- data/lib/secure_headers/railtie.rb +11 -6
- data/lib/secure_headers/utils/cookies_config.rb +95 -0
- data/lib/secure_headers/view_helper.rb +76 -5
- data/lib/secure_headers.rb +159 -99
- data/lib/tasks/tasks.rake +83 -0
- data/secure_headers.gemspec +13 -3
- data/spec/lib/secure_headers/configuration_spec.rb +24 -9
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +88 -189
- data/spec/lib/secure_headers/headers/cookie_spec.rb +182 -0
- data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +228 -0
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +73 -0
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +6 -5
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
- data/spec/lib/secure_headers/middleware_spec.rb +96 -6
- data/spec/lib/secure_headers/view_helpers_spec.rb +138 -0
- data/spec/lib/secure_headers_spec.rb +407 -58
- data/spec/spec_helper.rb +19 -12
- data/upgrading-to-3-0.md +13 -9
- data/upgrading-to-4-0.md +55 -0
- metadata +45 -9
- data/lib/secure_headers/padrino.rb +0 -13
- data/travis.sh +0 -10
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
2
3
|
|
|
3
4
|
module SecureHeaders
|
|
4
5
|
describe Configuration do
|
|
@@ -29,18 +30,24 @@ module SecureHeaders
|
|
|
29
30
|
expect_default_values(header_hash)
|
|
30
31
|
end
|
|
31
32
|
|
|
32
|
-
it "copies
|
|
33
|
+
it "copies config values when duping" do
|
|
33
34
|
Configuration.override(:test_override, Configuration::NOOP_CONFIGURATION) do
|
|
34
35
|
# do nothing, just copy it
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
config = Configuration.get(:test_override)
|
|
38
39
|
noop = Configuration.get(Configuration::NOOP_CONFIGURATION)
|
|
39
|
-
[:
|
|
40
|
-
|
|
40
|
+
[:csp, :csp_report_only, :cookies].each do |key|
|
|
41
|
+
expect(config.send(key)).to eq(noop.send(key))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
it "regenerates cached headers when building an override" do
|
|
46
|
+
Configuration.override(:test_override) do |config|
|
|
47
|
+
config.x_content_type_options = OPT_OUT
|
|
43
48
|
end
|
|
49
|
+
|
|
50
|
+
expect(Configuration.get.cached_headers).to_not eq(Configuration.get(:test_override).cached_headers)
|
|
44
51
|
end
|
|
45
52
|
|
|
46
53
|
it "stores an override of the global config" do
|
|
@@ -59,12 +66,12 @@ module SecureHeaders
|
|
|
59
66
|
default = Configuration.get
|
|
60
67
|
override = Configuration.get(:override)
|
|
61
68
|
|
|
62
|
-
expect(override.csp).not_to
|
|
69
|
+
expect(override.csp.directive_value(:default_src)).not_to be(default.csp.directive_value(:default_src))
|
|
63
70
|
end
|
|
64
71
|
|
|
65
72
|
it "allows you to override an override" do
|
|
66
73
|
Configuration.override(:override) do |config|
|
|
67
|
-
config.csp = { default_src: %w('self')}
|
|
74
|
+
config.csp = { default_src: %w('self'), script_src: %w('self')}
|
|
68
75
|
end
|
|
69
76
|
|
|
70
77
|
Configuration.override(:second_override, :override) do |config|
|
|
@@ -72,9 +79,17 @@ module SecureHeaders
|
|
|
72
79
|
end
|
|
73
80
|
|
|
74
81
|
original_override = Configuration.get(:override)
|
|
75
|
-
expect(original_override.csp).to eq(default_src: %w('self'))
|
|
82
|
+
expect(original_override.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self'))
|
|
76
83
|
override_config = Configuration.get(:second_override)
|
|
77
|
-
expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org))
|
|
84
|
+
expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "deprecates the secure_cookies configuration" do
|
|
88
|
+
expect {
|
|
89
|
+
Configuration.default do |config|
|
|
90
|
+
config.secure_cookies = true
|
|
91
|
+
end
|
|
92
|
+
}.to raise_error(ArgumentError)
|
|
78
93
|
end
|
|
79
94
|
end
|
|
80
95
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
3
|
+
|
|
4
|
+
module SecureHeaders
|
|
5
|
+
describe ClearSiteData do
|
|
6
|
+
describe "make_header" do
|
|
7
|
+
it "returns nil with nil config" do
|
|
8
|
+
expect(described_class.make_header).to be_nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "returns nil with empty config" do
|
|
12
|
+
expect(described_class.make_header([])).to be_nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "returns nil with opt-out config" do
|
|
16
|
+
expect(described_class.make_header(OPT_OUT)).to be_nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "returns all types with `true` config" do
|
|
20
|
+
name, value = described_class.make_header(true)
|
|
21
|
+
|
|
22
|
+
expect(name).to eq(ClearSiteData::HEADER_NAME)
|
|
23
|
+
expect(value).to eq(
|
|
24
|
+
%("cache", "cookies", "storage", "executionContexts")
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "returns specified types" do
|
|
29
|
+
name, value = described_class.make_header(["foo", "bar"])
|
|
30
|
+
|
|
31
|
+
expect(name).to eq(ClearSiteData::HEADER_NAME)
|
|
32
|
+
expect(value).to eq(%("foo", "bar"))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe "validate_config!" do
|
|
37
|
+
it "succeeds for `true` config" do
|
|
38
|
+
expect do
|
|
39
|
+
described_class.validate_config!(true)
|
|
40
|
+
end.not_to raise_error
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "succeeds for `nil` config" do
|
|
44
|
+
expect do
|
|
45
|
+
described_class.validate_config!(nil)
|
|
46
|
+
end.not_to raise_error
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "succeeds for opt-out config" do
|
|
50
|
+
expect do
|
|
51
|
+
described_class.validate_config!(OPT_OUT)
|
|
52
|
+
end.not_to raise_error
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "succeeds for empty config" do
|
|
56
|
+
expect do
|
|
57
|
+
described_class.validate_config!([])
|
|
58
|
+
end.not_to raise_error
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "succeeds for Array of Strings config" do
|
|
62
|
+
expect do
|
|
63
|
+
described_class.validate_config!(["foo"])
|
|
64
|
+
end.not_to raise_error
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "fails for Array of non-String config" do
|
|
68
|
+
expect do
|
|
69
|
+
described_class.validate_config!([1])
|
|
70
|
+
end.to raise_error(ClearSiteDataConfigError)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "fails for other types of config" do
|
|
74
|
+
expect do
|
|
75
|
+
described_class.validate_config!(:cookies)
|
|
76
|
+
end.to raise_error(ClearSiteDataConfigError)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe "make_header_value" do
|
|
81
|
+
it "returns a string of quoted values that are comma separated" do
|
|
82
|
+
value = described_class.make_header_value(["foo", "bar"])
|
|
83
|
+
expect(value).to eq(%("foo", "bar"))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
2
3
|
|
|
3
4
|
module SecureHeaders
|
|
4
5
|
describe ContentSecurityPolicy do
|
|
@@ -14,192 +15,21 @@ module SecureHeaders
|
|
|
14
15
|
|
|
15
16
|
describe "#name" do
|
|
16
17
|
context "when in report-only mode" do
|
|
17
|
-
specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(
|
|
18
|
+
specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME) }
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
context "when in enforce mode" do
|
|
21
|
-
specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(
|
|
22
|
+
specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(ContentSecurityPolicyConfig::HEADER_NAME) }
|
|
22
23
|
end
|
|
23
24
|
end
|
|
24
25
|
|
|
25
|
-
describe "#
|
|
26
|
-
it "
|
|
27
|
-
|
|
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)
|
|
26
|
+
describe "#value" do
|
|
27
|
+
it "uses a safe but non-breaking default value" do
|
|
28
|
+
expect(ContentSecurityPolicy.new.value).to eq("default-src https:; form-action 'self'; img-src https: data: 'self'; object-src 'none'; script-src https:; style-src 'self' 'unsafe-inline' https:")
|
|
183
29
|
end
|
|
184
|
-
end
|
|
185
30
|
|
|
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
|
-
describe "#value" do
|
|
201
31
|
it "discards 'none' values if any other source expressions are present" do
|
|
202
|
-
csp = ContentSecurityPolicy.new(default_opts.merge(
|
|
32
|
+
csp = ContentSecurityPolicy.new(default_opts.merge(child_src: %w('self' 'none')))
|
|
203
33
|
expect(csp.value).not_to include("'none'")
|
|
204
34
|
end
|
|
205
35
|
|
|
@@ -221,13 +51,18 @@ module SecureHeaders
|
|
|
221
51
|
expect(csp.value).to eq("default-src example.org")
|
|
222
52
|
end
|
|
223
53
|
|
|
54
|
+
it "does not build directives with a value of OPT_OUT (and bypasses directive requirements)" do
|
|
55
|
+
csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), script_src: OPT_OUT)
|
|
56
|
+
expect(csp.value).to eq("default-src example.org")
|
|
57
|
+
end
|
|
58
|
+
|
|
224
59
|
it "does not remove schemes from report-uri values" do
|
|
225
60
|
csp = ContentSecurityPolicy.new(default_src: %w(https:), report_uri: %w(https://example.org))
|
|
226
61
|
expect(csp.value).to eq("default-src https:; report-uri https://example.org")
|
|
227
62
|
end
|
|
228
63
|
|
|
229
64
|
it "does not remove schemes when :preserve_schemes is true" do
|
|
230
|
-
csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), :
|
|
65
|
+
csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), preserve_schemes: true)
|
|
231
66
|
expect(csp.value).to eq("default-src https://example.org")
|
|
232
67
|
end
|
|
233
68
|
|
|
@@ -246,42 +81,106 @@ module SecureHeaders
|
|
|
246
81
|
expect(csp.value).to eq("default-src example.org")
|
|
247
82
|
end
|
|
248
83
|
|
|
84
|
+
it "does add a boolean directive if the value is true" do
|
|
85
|
+
csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], block_all_mixed_content: true, upgrade_insecure_requests: true)
|
|
86
|
+
expect(csp.value).to eq("default-src example.org; block-all-mixed-content; upgrade-insecure-requests")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "does not add a boolean directive if the value is false" do
|
|
90
|
+
csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], block_all_mixed_content: true, upgrade_insecure_requests: false)
|
|
91
|
+
expect(csp.value).to eq("default-src example.org; block-all-mixed-content")
|
|
92
|
+
end
|
|
93
|
+
|
|
249
94
|
it "deduplicates any source expressions" do
|
|
250
95
|
csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org example.org))
|
|
251
96
|
expect(csp.value).to eq("default-src example.org")
|
|
252
97
|
end
|
|
253
98
|
|
|
99
|
+
it "creates maximally strict sandbox policy when passed no sandbox token values" do
|
|
100
|
+
csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: [])
|
|
101
|
+
expect(csp.value).to eq("default-src example.org; sandbox")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "creates maximally strict sandbox policy when passed true" do
|
|
105
|
+
csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: true)
|
|
106
|
+
expect(csp.value).to eq("default-src example.org; sandbox")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "creates sandbox policy when passed valid sandbox token values" do
|
|
110
|
+
csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: %w(allow-forms allow-scripts))
|
|
111
|
+
expect(csp.value).to eq("default-src example.org; sandbox allow-forms allow-scripts")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "does not emit a warning when using frame-src" do
|
|
115
|
+
expect(Kernel).to_not receive(:warn)
|
|
116
|
+
ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "raises an error when child-src and frame-src are supplied but are not equal" do
|
|
120
|
+
expect {
|
|
121
|
+
ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value
|
|
122
|
+
}.to raise_error(ArgumentError)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "supports strict-dynamic" do
|
|
126
|
+
csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}, USER_AGENTS[:chrome])
|
|
127
|
+
expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456'")
|
|
128
|
+
end
|
|
129
|
+
|
|
254
130
|
context "browser sniffing" do
|
|
255
131
|
let (:complex_opts) do
|
|
256
|
-
ContentSecurityPolicy::ALL_DIRECTIVES.each_with_object({}) do |directive, hash|
|
|
257
|
-
hash[directive] =
|
|
132
|
+
(ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash|
|
|
133
|
+
hash[directive] = ["#{directive.to_s.gsub("_", "-")}.com"]
|
|
258
134
|
end.merge({
|
|
259
135
|
block_all_mixed_content: true,
|
|
260
136
|
upgrade_insecure_requests: true,
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
137
|
+
script_src: %w(script-src.com),
|
|
138
|
+
script_nonce: 123456,
|
|
139
|
+
sandbox: %w(allow-forms),
|
|
140
|
+
plugin_types: %w(application/pdf)
|
|
264
141
|
})
|
|
265
142
|
end
|
|
266
143
|
|
|
267
144
|
it "does not filter any directives for Chrome" do
|
|
268
145
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
|
|
269
|
-
expect(policy.value).to eq("default-src
|
|
146
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
270
147
|
end
|
|
271
148
|
|
|
272
149
|
it "does not filter any directives for Opera" do
|
|
273
150
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
|
|
274
|
-
expect(policy.value).to eq("default-src
|
|
151
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
275
152
|
end
|
|
276
153
|
|
|
277
154
|
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
|
|
278
155
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
|
|
279
|
-
expect(policy.value).to eq("default-src
|
|
156
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
|
|
160
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
|
|
161
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "child-src value is copied to frame-src, 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 Edge" do
|
|
165
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
|
|
166
|
+
expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
280
167
|
end
|
|
281
168
|
|
|
282
|
-
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
|
|
169
|
+
it "child-src value is copied to frame-src, 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
|
|
283
170
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
|
|
284
|
-
expect(policy.value).to eq("default-src
|
|
171
|
+
expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do
|
|
175
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10])
|
|
176
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; report-uri report-uri.com")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it "falls back to standard Firefox defaults when the useragent version is not present" do
|
|
180
|
+
ua = USER_AGENTS[:firefox].dup
|
|
181
|
+
allow(ua).to receive(:version).and_return(nil)
|
|
182
|
+
policy = ContentSecurityPolicy.new(complex_opts, ua)
|
|
183
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
285
184
|
end
|
|
286
185
|
end
|
|
287
186
|
end
|