secure_headers 2.2.0 → 3.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/.rspec +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +3 -1
- data/CHANGELOG.md +205 -0
- data/Gemfile +9 -10
- data/Guardfile +12 -0
- data/README.md +175 -276
- data/Rakefile +2 -36
- data/lib/secure_headers/configuration.rb +189 -0
- data/lib/secure_headers/headers/content_security_policy.rb +367 -188
- data/lib/secure_headers/headers/public_key_pins.rb +43 -57
- data/lib/secure_headers/headers/strict_transport_security.rb +21 -47
- data/lib/secure_headers/headers/x_content_type_options.rb +19 -32
- data/lib/secure_headers/headers/x_download_options.rb +18 -31
- data/lib/secure_headers/headers/x_frame_options.rb +24 -32
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +19 -32
- data/lib/secure_headers/headers/x_xss_protection.rb +17 -46
- data/lib/secure_headers/middleware.rb +15 -0
- data/lib/secure_headers/padrino.rb +1 -2
- data/lib/secure_headers/railtie.rb +9 -6
- data/lib/secure_headers/view_helper.rb +27 -44
- data/lib/secure_headers.rb +244 -145
- data/secure_headers.gemspec +7 -11
- data/spec/lib/secure_headers/configuration_spec.rb +80 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +176 -213
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +17 -17
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +11 -43
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +13 -18
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +13 -17
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +15 -17
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +22 -39
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +22 -30
- data/spec/lib/secure_headers/middleware_spec.rb +40 -0
- data/spec/lib/secure_headers_spec.rb +204 -278
- data/spec/spec_helper.rb +32 -25
- data/upgrading-to-3-0.md +37 -0
- metadata +28 -95
- data/fixtures/rails_3_2_12/.rspec +0 -1
- data/fixtures/rails_3_2_12/Gemfile +0 -6
- data/fixtures/rails_3_2_12/README.rdoc +0 -261
- data/fixtures/rails_3_2_12/Rakefile +0 -7
- data/fixtures/rails_3_2_12/app/controllers/application_controller.rb +0 -4
- data/fixtures/rails_3_2_12/app/controllers/other_things_controller.rb +0 -5
- data/fixtures/rails_3_2_12/app/controllers/things_controller.rb +0 -5
- data/fixtures/rails_3_2_12/app/models/.gitkeep +0 -0
- data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +0 -11
- data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +0 -2
- data/fixtures/rails_3_2_12/app/views/things/index.html.erb +0 -1
- data/fixtures/rails_3_2_12/config/application.rb +0 -14
- data/fixtures/rails_3_2_12/config/boot.rb +0 -6
- data/fixtures/rails_3_2_12/config/environment.rb +0 -5
- data/fixtures/rails_3_2_12/config/environments/test.rb +0 -37
- data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +0 -17
- data/fixtures/rails_3_2_12/config/routes.rb +0 -4
- data/fixtures/rails_3_2_12/config/script_hashes.yml +0 -5
- data/fixtures/rails_3_2_12/config.ru +0 -7
- data/fixtures/rails_3_2_12/lib/assets/.gitkeep +0 -0
- data/fixtures/rails_3_2_12/lib/tasks/.gitkeep +0 -0
- data/fixtures/rails_3_2_12/log/.gitkeep +0 -0
- data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +0 -83
- data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +0 -54
- data/fixtures/rails_3_2_12/spec/spec_helper.rb +0 -15
- data/fixtures/rails_3_2_12/vendor/assets/javascripts/.gitkeep +0 -0
- data/fixtures/rails_3_2_12/vendor/assets/stylesheets/.gitkeep +0 -0
- data/fixtures/rails_3_2_12/vendor/plugins/.gitkeep +0 -0
- data/fixtures/rails_3_2_12_no_init/.rspec +0 -1
- data/fixtures/rails_3_2_12_no_init/Gemfile +0 -5
- data/fixtures/rails_3_2_12_no_init/README.rdoc +0 -261
- data/fixtures/rails_3_2_12_no_init/Rakefile +0 -7
- data/fixtures/rails_3_2_12_no_init/app/controllers/application_controller.rb +0 -4
- data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +0 -6
- data/fixtures/rails_3_2_12_no_init/app/controllers/things_controller.rb +0 -5
- data/fixtures/rails_3_2_12_no_init/app/models/.gitkeep +0 -0
- data/fixtures/rails_3_2_12_no_init/app/views/layouts/application.html.erb +0 -12
- data/fixtures/rails_3_2_12_no_init/app/views/other_things/index.html.erb +0 -1
- data/fixtures/rails_3_2_12_no_init/app/views/things/index.html.erb +0 -0
- data/fixtures/rails_3_2_12_no_init/config/application.rb +0 -17
- data/fixtures/rails_3_2_12_no_init/config/boot.rb +0 -6
- data/fixtures/rails_3_2_12_no_init/config/environment.rb +0 -5
- data/fixtures/rails_3_2_12_no_init/config/environments/test.rb +0 -37
- data/fixtures/rails_3_2_12_no_init/config/routes.rb +0 -4
- data/fixtures/rails_3_2_12_no_init/config.ru +0 -4
- data/fixtures/rails_3_2_12_no_init/lib/assets/.gitkeep +0 -0
- data/fixtures/rails_3_2_12_no_init/lib/tasks/.gitkeep +0 -0
- data/fixtures/rails_3_2_12_no_init/log/.gitkeep +0 -0
- data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +0 -50
- data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +0 -54
- data/fixtures/rails_3_2_12_no_init/spec/spec_helper.rb +0 -5
- data/fixtures/rails_3_2_12_no_init/vendor/assets/javascripts/.gitkeep +0 -0
- data/fixtures/rails_3_2_12_no_init/vendor/assets/stylesheets/.gitkeep +0 -0
- data/fixtures/rails_3_2_12_no_init/vendor/plugins/.gitkeep +0 -0
- data/fixtures/rails_4_1_8/Gemfile +0 -5
- data/fixtures/rails_4_1_8/README.rdoc +0 -28
- data/fixtures/rails_4_1_8/Rakefile +0 -6
- data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +0 -4
- data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
- data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +0 -5
- data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +0 -5
- data/fixtures/rails_4_1_8/app/models/.keep +0 -0
- data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
- data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +0 -11
- data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +0 -2
- data/fixtures/rails_4_1_8/app/views/things/index.html.erb +0 -1
- data/fixtures/rails_4_1_8/config/application.rb +0 -15
- data/fixtures/rails_4_1_8/config/boot.rb +0 -4
- data/fixtures/rails_4_1_8/config/environment.rb +0 -5
- data/fixtures/rails_4_1_8/config/environments/test.rb +0 -10
- data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +0 -17
- data/fixtures/rails_4_1_8/config/routes.rb +0 -4
- data/fixtures/rails_4_1_8/config/script_hashes.yml +0 -5
- data/fixtures/rails_4_1_8/config/secrets.yml +0 -22
- data/fixtures/rails_4_1_8/config.ru +0 -4
- data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
- data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
- data/fixtures/rails_4_1_8/log/.keep +0 -0
- data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +0 -83
- data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +0 -59
- data/fixtures/rails_4_1_8/spec/spec_helper.rb +0 -15
- data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
- data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
- data/lib/secure_headers/hash_helper.rb +0 -7
- data/lib/secure_headers/header.rb +0 -5
- data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +0 -22
- data/lib/secure_headers/version.rb +0 -3
- data/lib/tasks/tasks.rake +0 -48
- data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +0 -47
|
@@ -2,278 +2,241 @@ require 'spec_helper'
|
|
|
2
2
|
|
|
3
3
|
module SecureHeaders
|
|
4
4
|
describe ContentSecurityPolicy do
|
|
5
|
-
let(:default_opts) do
|
|
5
|
+
let (:default_opts) do
|
|
6
6
|
{
|
|
7
|
-
:
|
|
8
|
-
:
|
|
9
|
-
:
|
|
10
|
-
:
|
|
11
|
-
:
|
|
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
12
|
}
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
CHROME_25 = "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/537.22 (KHTML like Gecko) Chrome/25.0.1364.99 Safari/537.22"
|
|
20
|
-
|
|
15
|
+
describe "#name" do
|
|
16
|
+
context "when in report-only mode" do
|
|
17
|
+
specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(ContentSecurityPolicy::HEADER_NAME + "-Report-Only") }
|
|
18
|
+
end
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
context "when in enforce mode" do
|
|
21
|
+
specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(ContentSecurityPolicy::HEADER_NAME) }
|
|
22
|
+
end
|
|
24
23
|
end
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
}
|
|
29
51
|
|
|
30
|
-
|
|
31
|
-
context "when supplying options to override request" do
|
|
32
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => IE).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
33
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
34
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX_23).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
35
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
36
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME_25).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
52
|
+
CSP.validate_config!(config)
|
|
37
53
|
end
|
|
38
54
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
44
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME + "-Report-Only")}
|
|
55
|
+
it "requires a :default_src value" do
|
|
56
|
+
expect do
|
|
57
|
+
CSP.validate_config!(script_src: %('self'))
|
|
58
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
45
59
|
end
|
|
46
60
|
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
49
66
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME)}
|
|
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)
|
|
55
71
|
end
|
|
56
|
-
end
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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)
|
|
62
77
|
end
|
|
63
|
-
end
|
|
64
78
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
@opts = default_opts
|
|
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)
|
|
70
83
|
end
|
|
71
84
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
77
90
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
83
96
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
end
|
|
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
|
|
89
104
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
95
112
|
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
}
|
|
98
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
|
|
99
124
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
:
|
|
104
|
-
:
|
|
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
|
|
105
130
|
}
|
|
106
|
-
|
|
107
|
-
csp = ContentSecurityPolicy.new(opts)
|
|
108
|
-
expect(csp.value).to eq("default-src http://lambda/result; img-src http://lambda/result data:;")
|
|
109
|
-
expect(csp.name).to match("Content-Security-Policy")
|
|
110
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
|
|
111
136
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
allow(controller).to receive(:current_user).and_return(user)
|
|
117
|
-
opts = {
|
|
118
|
-
:disable_fill_missing => true,
|
|
119
|
-
:default_src => "self",
|
|
120
|
-
:enforce => lambda { |c| c.current_user.beta_testing? }
|
|
121
|
-
}
|
|
122
|
-
csp = ContentSecurityPolicy.new(opts, :controller => controller)
|
|
123
|
-
expect(csp.name).to match("Content-Security-Policy")
|
|
137
|
+
it "raises an error if appending to a OPT_OUT policy" do
|
|
138
|
+
Configuration.default do |config|
|
|
139
|
+
config.csp = OPT_OUT
|
|
124
140
|
end
|
|
141
|
+
expect do
|
|
142
|
+
CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com))
|
|
143
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
125
144
|
end
|
|
126
145
|
end
|
|
127
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
|
+
|
|
128
161
|
describe "#value" do
|
|
129
|
-
it "
|
|
130
|
-
csp = ContentSecurityPolicy.new(
|
|
131
|
-
expect
|
|
132
|
-
csp.value
|
|
133
|
-
}.to raise_error(RuntimeError)
|
|
162
|
+
it "discards 'none' values if any other source expressions are present" do
|
|
163
|
+
csp = ContentSecurityPolicy.new(default_opts.merge(frame_src: %w('self' 'none')))
|
|
164
|
+
expect(csp.value).not_to include("'none'")
|
|
134
165
|
end
|
|
135
166
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
it "supports all level 2 directives" do
|
|
140
|
-
directive_name = ::SecureHeaders::ContentSecurityPolicy.send(:symbol_to_hyphen_case, non_default_source)
|
|
141
|
-
config.merge!({ non_default_source => "value" })
|
|
142
|
-
csp = ContentSecurityPolicy.new(config, :request => request_for(CHROME))
|
|
143
|
-
expect(csp.value).to match(/#{directive_name} value;/)
|
|
144
|
-
end
|
|
145
|
-
end
|
|
167
|
+
it "discards source expressions (besides unsafe-* and non-host source values) when * is present" do
|
|
168
|
+
csp = ContentSecurityPolicy.new(default_src: %w(* 'unsafe-inline' 'unsafe-eval' http: https: example.org data: blob:))
|
|
169
|
+
expect(csp.value).to eq("default-src * 'unsafe-inline' 'unsafe-eval' data: blob:")
|
|
146
170
|
end
|
|
147
171
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
it "appends the value if img-src is specified" do
|
|
155
|
-
csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME))
|
|
156
|
-
expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
|
|
157
|
-
end
|
|
172
|
+
it "minifies source expressions based on overlapping wildcards" do
|
|
173
|
+
config = {
|
|
174
|
+
default_src: %w(a.example.org b.example.org *.example.org https://*.example.org)
|
|
175
|
+
}
|
|
176
|
+
csp = ContentSecurityPolicy.new(config)
|
|
177
|
+
expect(csp.value).to eq("default-src *.example.org")
|
|
158
178
|
end
|
|
159
179
|
|
|
160
|
-
it "
|
|
161
|
-
|
|
162
|
-
csp
|
|
163
|
-
value = "default-src https:; connect-src https:; font-src https:; frame-src https:; img-src https: data:; media-src https:; object-src https:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;"
|
|
164
|
-
expect(csp.value).to eq(value)
|
|
180
|
+
it "removes http/s schemes from hosts" do
|
|
181
|
+
csp = ContentSecurityPolicy.new(default_src: %w(https://example.org))
|
|
182
|
+
expect(csp.value).to eq("default-src example.org")
|
|
165
183
|
end
|
|
166
184
|
|
|
167
|
-
it "
|
|
168
|
-
csp = ContentSecurityPolicy.new(
|
|
169
|
-
expect(csp.value).to
|
|
185
|
+
it "does not remove schemes from report-uri values" do
|
|
186
|
+
csp = ContentSecurityPolicy.new(default_src: %w(https:), report_uri: %w(https://example.org))
|
|
187
|
+
expect(csp.value).to eq("default-src https:; report-uri https://example.org")
|
|
170
188
|
end
|
|
171
189
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
expect(csp.value).to eq("default-src https:; img-src https: data:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;")
|
|
176
|
-
end
|
|
190
|
+
it "does not remove schemes when :preserve_schemes is true" do
|
|
191
|
+
csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), :preserve_schemes => true)
|
|
192
|
+
expect(csp.value).to eq("default-src https://example.org")
|
|
177
193
|
end
|
|
178
194
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
expect(csp.value).to eq("default-src https:; img-src https: data:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;")
|
|
183
|
-
end
|
|
195
|
+
it "removes nil from source lists" do
|
|
196
|
+
csp = ContentSecurityPolicy.new(default_src: ["https://example.org", nil])
|
|
197
|
+
expect(csp.value).to eq("default-src example.org")
|
|
184
198
|
end
|
|
185
199
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
it "adds a nonce and unsafe-inline to the style-src value" do
|
|
193
|
-
header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce"), :request => request_for(CHROME))
|
|
194
|
-
expect(header.value).to include("style-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
it "adds an identical nonce to the style and script-src directives" do
|
|
198
|
-
header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce", :script_src => "self nonce"), :request => request_for(CHROME))
|
|
199
|
-
nonce = header.nonce
|
|
200
|
-
value = header.value
|
|
201
|
-
expect(value).to include("style-src 'self' 'nonce-#{nonce}' 'unsafe-inline'")
|
|
202
|
-
expect(value).to include("script-src 'self' 'nonce-#{nonce}' 'unsafe-inline'")
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
it "does not add 'unsafe-inline' twice" do
|
|
206
|
-
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce inline"), :request => request_for(CHROME))
|
|
207
|
-
expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline';")
|
|
208
|
-
end
|
|
200
|
+
it "does not add a directive if the value is an empty array (or all nil)" do
|
|
201
|
+
csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], script_src: [nil])
|
|
202
|
+
expect(csp.value).to eq("default-src example.org")
|
|
209
203
|
end
|
|
210
204
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
:frame_src => "http:",
|
|
216
|
-
:img_src => "http:"
|
|
217
|
-
}
|
|
218
|
-
})
|
|
219
|
-
}
|
|
205
|
+
it "does not add a directive if the value is nil" do
|
|
206
|
+
csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], script_src: nil)
|
|
207
|
+
expect(csp.value).to eq("default-src example.org")
|
|
208
|
+
end
|
|
220
209
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
210
|
+
it "deduplicates any source expressions" do
|
|
211
|
+
csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org example.org))
|
|
212
|
+
expect(csp.value).to eq("default-src example.org")
|
|
213
|
+
end
|
|
225
214
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
215
|
+
context "browser sniffing" do
|
|
216
|
+
let (:complex_opts) do
|
|
217
|
+
ContentSecurityPolicy::ALL_DIRECTIVES.each_with_object({}) { |directive, hash| hash[directive] = %w('self') }
|
|
218
|
+
.merge(block_all_mixed_content: true, reflected_xss: "block")
|
|
219
|
+
.merge(script_src: %w('self'), script_nonce: 123456)
|
|
229
220
|
end
|
|
230
221
|
|
|
231
|
-
it "does not
|
|
232
|
-
|
|
233
|
-
expect(
|
|
222
|
+
it "does not filter any directives for Chrome" do
|
|
223
|
+
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'")
|
|
234
225
|
end
|
|
235
|
-
end
|
|
236
226
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
double.tap do |env|
|
|
241
|
-
allow(env).to receive(:[]).with('HTTP_USER_AGENT').and_return(ua)
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
let(:request) do
|
|
245
|
-
double(
|
|
246
|
-
:ssl? => true,
|
|
247
|
-
:url => 'https://example.com',
|
|
248
|
-
:env => env
|
|
249
|
-
)
|
|
227
|
+
it "does not filter any directives for Opera" do
|
|
228
|
+
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'")
|
|
250
230
|
end
|
|
251
231
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
let(:options) { {:controller => controller} }
|
|
256
|
-
|
|
257
|
-
it "adds metadata to env" do
|
|
258
|
-
metadata = {
|
|
259
|
-
:config => config,
|
|
260
|
-
:options => options
|
|
261
|
-
}
|
|
262
|
-
expect(ContentSecurityPolicy).to receive(:options_from_request).and_return(options)
|
|
263
|
-
expect(env).to receive(:[]=).with(ContentSecurityPolicy::ENV_KEY, metadata)
|
|
264
|
-
ContentSecurityPolicy.add_to_env(request, controller, config)
|
|
265
|
-
end
|
|
232
|
+
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
|
|
233
|
+
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'")
|
|
266
235
|
end
|
|
267
236
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
expect(options).to eql({
|
|
272
|
-
:ua => ua,
|
|
273
|
-
:ssl => true,
|
|
274
|
-
:request_uri => 'https://example.com'
|
|
275
|
-
})
|
|
276
|
-
end
|
|
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
|
|
238
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
|
|
239
|
+
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'")
|
|
277
240
|
end
|
|
278
241
|
end
|
|
279
242
|
end
|
|
@@ -2,35 +2,35 @@ require 'spec_helper'
|
|
|
2
2
|
|
|
3
3
|
module SecureHeaders
|
|
4
4
|
describe PublicKeyPins do
|
|
5
|
-
specify{ expect(PublicKeyPins.new(:
|
|
6
|
-
specify{ expect(PublicKeyPins.new(:
|
|
5
|
+
specify { expect(PublicKeyPins.new(max_age: 1234, report_only: true).name).to eq("Public-Key-Pins-Report-Only") }
|
|
6
|
+
specify { expect(PublicKeyPins.new(max_age: 1234).name).to eq("Public-Key-Pins") }
|
|
7
7
|
|
|
8
|
-
specify { expect(PublicKeyPins.new(
|
|
9
|
-
specify { expect(PublicKeyPins.new(:
|
|
10
|
-
specify
|
|
11
|
-
config = {:
|
|
8
|
+
specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") }
|
|
9
|
+
specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") }
|
|
10
|
+
specify do
|
|
11
|
+
config = { max_age: 1234, pins: [{ sha256: 'base64encodedpin1' }, { sha256: 'base64encodedpin2' }] }
|
|
12
12
|
header_value = "max-age=1234; pin-sha256=\"base64encodedpin1\"; pin-sha256=\"base64encodedpin2\""
|
|
13
13
|
expect(PublicKeyPins.new(config).value).to eq(header_value)
|
|
14
|
-
|
|
14
|
+
end
|
|
15
15
|
|
|
16
16
|
context "with an invalid configuration" do
|
|
17
17
|
it "raises an exception when max-age is not provided" do
|
|
18
|
-
expect
|
|
19
|
-
PublicKeyPins.
|
|
20
|
-
|
|
18
|
+
expect do
|
|
19
|
+
PublicKeyPins.validate_config!(foo: 'bar')
|
|
20
|
+
end.to raise_error(PublicKeyPinsConfigError)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
it "raises an exception with an invalid max-age" do
|
|
24
|
-
expect
|
|
25
|
-
PublicKeyPins.
|
|
26
|
-
|
|
24
|
+
expect do
|
|
25
|
+
PublicKeyPins.validate_config!(max_age: 'abc123')
|
|
26
|
+
end.to raise_error(PublicKeyPinsConfigError)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
it 'raises an exception with less than 2 pins' do
|
|
30
|
-
expect
|
|
31
|
-
config = {:
|
|
32
|
-
PublicKeyPins.
|
|
33
|
-
|
|
30
|
+
expect do
|
|
31
|
+
config = { max_age: 1234, pins: [{ sha256: 'base64encodedpin' }] }
|
|
32
|
+
PublicKeyPins.validate_config!(config)
|
|
33
|
+
end.to raise_error(PublicKeyPinsConfigError)
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
end
|
|
@@ -2,60 +2,28 @@ require 'spec_helper'
|
|
|
2
2
|
|
|
3
3
|
module SecureHeaders
|
|
4
4
|
describe StrictTransportSecurity do
|
|
5
|
-
specify{ expect(StrictTransportSecurity.new.name).to eq("Strict-Transport-Security") }
|
|
6
|
-
|
|
7
5
|
describe "#value" do
|
|
8
|
-
specify { expect(StrictTransportSecurity.
|
|
9
|
-
specify { expect(StrictTransportSecurity.
|
|
10
|
-
specify { expect(StrictTransportSecurity.new(:max_age => '1234').value).to eq("max-age=1234")}
|
|
11
|
-
specify { expect(StrictTransportSecurity.new(:max_age => 1234).value).to eq("max-age=1234")}
|
|
12
|
-
specify { expect(StrictTransportSecurity.new(:max_age => HSTS_MAX_AGE, :include_subdomains => true).value).to eq("max-age=#{HSTS_MAX_AGE}; includeSubdomains")}
|
|
13
|
-
specify { expect(StrictTransportSecurity.new(:max_age => HSTS_MAX_AGE, :include_subdomains => true, :preload => true).value).to eq("max-age=#{HSTS_MAX_AGE}; includeSubdomains; preload")}
|
|
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"]) }
|
|
14
8
|
|
|
15
9
|
context "with an invalid configuration" do
|
|
16
|
-
context "with a hash argument" do
|
|
17
|
-
it "should allow string values for max-age" do
|
|
18
|
-
expect {
|
|
19
|
-
StrictTransportSecurity.new(:max_age => '1234')
|
|
20
|
-
}.not_to raise_error
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
it "should allow integer values for max-age" do
|
|
24
|
-
expect {
|
|
25
|
-
StrictTransportSecurity.new(:max_age => 1234)
|
|
26
|
-
}.not_to raise_error
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
it "raises an exception with an invalid max-age" do
|
|
30
|
-
expect {
|
|
31
|
-
StrictTransportSecurity.new(:max_age => 'abc123')
|
|
32
|
-
}.to raise_error(STSBuildError)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
it "raises an exception if max-age is not supplied" do
|
|
36
|
-
expect {
|
|
37
|
-
StrictTransportSecurity.new(:includeSubdomains => true)
|
|
38
|
-
}.to raise_error(STSBuildError)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
10
|
context "with a string argument" do
|
|
43
11
|
it "raises an exception with an invalid max-age" do
|
|
44
|
-
expect
|
|
45
|
-
StrictTransportSecurity.
|
|
46
|
-
|
|
12
|
+
expect do
|
|
13
|
+
StrictTransportSecurity.validate_config!('max-age=abc123')
|
|
14
|
+
end.to raise_error(STSConfigError)
|
|
47
15
|
end
|
|
48
16
|
|
|
49
17
|
it "raises an exception if max-age is not supplied" do
|
|
50
|
-
expect
|
|
51
|
-
StrictTransportSecurity.
|
|
52
|
-
|
|
18
|
+
expect do
|
|
19
|
+
StrictTransportSecurity.validate_config!('includeSubdomains')
|
|
20
|
+
end.to raise_error(STSConfigError)
|
|
53
21
|
end
|
|
54
22
|
|
|
55
23
|
it "raises an exception with an invalid format" do
|
|
56
|
-
expect
|
|
57
|
-
StrictTransportSecurity.
|
|
58
|
-
|
|
24
|
+
expect do
|
|
25
|
+
StrictTransportSecurity.validate_config!('max-age=123includeSubdomains')
|
|
26
|
+
end.to raise_error(STSConfigError)
|
|
59
27
|
end
|
|
60
28
|
end
|
|
61
29
|
end
|