secure_headers 2.5.3 → 3.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of secure_headers might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.travis.yml +2 -1
- data/Gemfile +9 -16
- data/README.md +154 -331
- data/Rakefile +2 -36
- data/lib/secure_headers/configuration.rb +189 -0
- data/lib/secure_headers/headers/content_security_policy.rb +341 -254
- data/lib/secure_headers/headers/public_key_pins.rb +43 -58
- data/lib/secure_headers/headers/strict_transport_security.rb +21 -49
- data/lib/secure_headers/headers/x_content_type_options.rb +18 -33
- data/lib/secure_headers/headers/x_download_options.rb +18 -33
- data/lib/secure_headers/headers/x_frame_options.rb +24 -34
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +19 -34
- data/lib/secure_headers/headers/x_xss_protection.rb +17 -48
- 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 -43
- data/lib/secure_headers.rb +254 -61
- data/secure_headers.gemspec +7 -12
- data/spec/lib/secure_headers/configuration_spec.rb +80 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +111 -276
- 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 +11 -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 +20 -30
- data/spec/lib/secure_headers/middleware_spec.rb +40 -0
- data/spec/lib/secure_headers_spec.rb +201 -339
- data/spec/spec_helper.rb +30 -30
- data/upgrading-to-3-0.md +35 -0
- metadata +14 -100
- data/fixtures/rails_3_2_22/.rspec +0 -1
- data/fixtures/rails_3_2_22/Gemfile +0 -6
- data/fixtures/rails_3_2_22/README.rdoc +0 -261
- data/fixtures/rails_3_2_22/Rakefile +0 -7
- data/fixtures/rails_3_2_22/app/controllers/application_controller.rb +0 -4
- data/fixtures/rails_3_2_22/app/controllers/other_things_controller.rb +0 -5
- data/fixtures/rails_3_2_22/app/controllers/things_controller.rb +0 -5
- data/fixtures/rails_3_2_22/app/models/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/app/views/layouts/application.html.erb +0 -11
- data/fixtures/rails_3_2_22/app/views/other_things/index.html.erb +0 -2
- data/fixtures/rails_3_2_22/app/views/things/index.html.erb +0 -1
- data/fixtures/rails_3_2_22/config/application.rb +0 -14
- data/fixtures/rails_3_2_22/config/boot.rb +0 -6
- data/fixtures/rails_3_2_22/config/environment.rb +0 -5
- data/fixtures/rails_3_2_22/config/environments/test.rb +0 -37
- data/fixtures/rails_3_2_22/config/initializers/secure_headers.rb +0 -16
- data/fixtures/rails_3_2_22/config/routes.rb +0 -4
- data/fixtures/rails_3_2_22/config/script_hashes.yml +0 -5
- data/fixtures/rails_3_2_22/config.ru +0 -7
- data/fixtures/rails_3_2_22/lib/assets/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/lib/tasks/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/log/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/spec/controllers/other_things_controller_spec.rb +0 -83
- data/fixtures/rails_3_2_22/spec/controllers/things_controller_spec.rb +0 -54
- data/fixtures/rails_3_2_22/spec/spec_helper.rb +0 -15
- data/fixtures/rails_3_2_22/vendor/assets/javascripts/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/vendor/assets/stylesheets/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/vendor/plugins/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/.rspec +0 -1
- data/fixtures/rails_3_2_22_no_init/Gemfile +0 -6
- data/fixtures/rails_3_2_22_no_init/README.rdoc +0 -261
- data/fixtures/rails_3_2_22_no_init/Rakefile +0 -7
- data/fixtures/rails_3_2_22_no_init/app/controllers/application_controller.rb +0 -4
- data/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb +0 -20
- data/fixtures/rails_3_2_22_no_init/app/controllers/things_controller.rb +0 -5
- data/fixtures/rails_3_2_22_no_init/app/models/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/app/views/layouts/application.html.erb +0 -12
- data/fixtures/rails_3_2_22_no_init/app/views/other_things/index.html.erb +0 -1
- data/fixtures/rails_3_2_22_no_init/app/views/things/index.html.erb +0 -0
- data/fixtures/rails_3_2_22_no_init/config/application.rb +0 -17
- data/fixtures/rails_3_2_22_no_init/config/boot.rb +0 -6
- data/fixtures/rails_3_2_22_no_init/config/environment.rb +0 -5
- data/fixtures/rails_3_2_22_no_init/config/environments/test.rb +0 -37
- data/fixtures/rails_3_2_22_no_init/config/routes.rb +0 -4
- data/fixtures/rails_3_2_22_no_init/config.ru +0 -4
- data/fixtures/rails_3_2_22_no_init/lib/assets/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/lib/tasks/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/log/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/spec/controllers/other_things_controller_spec.rb +0 -56
- data/fixtures/rails_3_2_22_no_init/spec/controllers/things_controller_spec.rb +0 -54
- data/fixtures/rails_3_2_22_no_init/spec/spec_helper.rb +0 -5
- data/fixtures/rails_3_2_22_no_init/vendor/assets/javascripts/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/vendor/assets/stylesheets/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_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 -16
- 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/controller_extension.rb +0 -158
- 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 -46
@@ -2,336 +2,171 @@ 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
|
-
let(:controller) { DummyClass.new }
|
15
|
-
|
16
|
-
IE = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"
|
17
|
-
FIREFOX = "Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.2) Gecko/20121223 Ubuntu/9.25 (jaunty) Firefox/3.8"
|
18
|
-
FIREFOX_23 = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:23.0) Gecko/20131011 Firefox/23.0"
|
19
|
-
CHROME = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4"
|
20
|
-
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"
|
21
|
-
SAFARI = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A"
|
22
|
-
OPERA = "Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16"
|
23
|
-
|
24
|
-
def request_for user_agent, request_uri=nil, options={:ssl => false}
|
25
|
-
double(:ssl? => options[:ssl], :env => {'HTTP_USER_AGENT' => user_agent}, :url => (request_uri || 'http://areallylongdomainexample.com') )
|
26
|
-
end
|
27
|
-
|
28
|
-
before(:each) do
|
29
|
-
@options_with_forwarding = default_opts.merge(:report_uri => 'https://example.com/csp', :forward_endpoint => 'https://anotherexample.com')
|
30
|
-
end
|
31
14
|
|
32
15
|
describe "#name" do
|
33
|
-
context "when supplying options to override request" do
|
34
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => IE).name).to eq(HEADER_NAME + "-Report-Only")}
|
35
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX).name).to eq(HEADER_NAME + "-Report-Only")}
|
36
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX_23).name).to eq(HEADER_NAME + "-Report-Only")}
|
37
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME).name).to eq(HEADER_NAME + "-Report-Only")}
|
38
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME_25).name).to eq(HEADER_NAME + "-Report-Only")}
|
39
|
-
end
|
40
|
-
|
41
16
|
context "when in report-only mode" do
|
42
|
-
specify { expect(ContentSecurityPolicy.new(default_opts
|
43
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX)).name).to eq(HEADER_NAME + "-Report-Only")}
|
44
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX_23)).name).to eq(HEADER_NAME + "-Report-Only")}
|
45
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME + "-Report-Only")}
|
46
|
-
specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME + "-Report-Only")}
|
17
|
+
specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(ContentSecurityPolicy::HEADER_NAME + "-Report-Only") }
|
47
18
|
end
|
48
19
|
|
49
20
|
context "when in enforce mode" do
|
50
|
-
|
51
|
-
|
52
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(IE)).name).to eq(HEADER_NAME)}
|
53
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX)).name).to eq(HEADER_NAME)}
|
54
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX_23)).name).to eq(HEADER_NAME)}
|
55
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME)}
|
56
|
-
specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME)}
|
21
|
+
specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(ContentSecurityPolicy::HEADER_NAME) }
|
57
22
|
end
|
58
23
|
end
|
59
24
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
it "imports JSON to build a policy" do
|
67
|
-
json1 = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"]})
|
68
|
-
json2 = %({"style-src":["'unsafe-inline'"],"img-src":["https:","data:"]})
|
69
|
-
json3 = %({"style-src":["https:","about:"]})
|
70
|
-
config = ContentSecurityPolicy.from_json(json1, json2, json3)
|
71
|
-
policy = ContentSecurityPolicy.new(config)
|
25
|
+
describe "#validate_config!" do
|
26
|
+
it "requires a :default_src value" do
|
27
|
+
expect do
|
28
|
+
CSP.validate_config!(script_src: %('self'))
|
29
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
30
|
+
end
|
72
31
|
|
73
|
-
|
74
|
-
|
75
|
-
|
32
|
+
it "requires :report_only to be a truthy value" do
|
33
|
+
expect do
|
34
|
+
CSP.validate_config!(default_opts.merge(report_only: "steve"))
|
35
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
36
|
+
end
|
76
37
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
38
|
+
it "requires :block_all_mixed_content to be a boolean value" do
|
39
|
+
expect do
|
40
|
+
CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve"))
|
41
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
81
42
|
end
|
82
|
-
end
|
83
43
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
44
|
+
it "requires all source lists to be an array of strings" do
|
45
|
+
expect do
|
46
|
+
CSP.validate_config!(default_src: "steve")
|
47
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
88
48
|
end
|
89
49
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
50
|
+
it "rejects unknown directives / config" do
|
51
|
+
expect do
|
52
|
+
CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve")
|
53
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
54
|
+
end
|
95
55
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
end
|
56
|
+
# this is mostly to ensure people don't use the antiquated shorthands common in other configs
|
57
|
+
it "performs light validation on source lists" do
|
58
|
+
expect do
|
59
|
+
CSP.validate_config!(default_src: %w(self none inline eval))
|
60
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
61
|
+
end
|
62
|
+
end
|
101
63
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
64
|
+
describe "#combine_policies" do
|
65
|
+
it "combines the default-src value with the override if the directive was unconfigured" do
|
66
|
+
combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com))
|
67
|
+
csp = ContentSecurityPolicy.new(combined_config)
|
68
|
+
expect(csp.name).to eq(CSP::HEADER_NAME)
|
69
|
+
expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com")
|
70
|
+
end
|
107
71
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
:
|
72
|
+
it "overrides the report_only flag" do
|
73
|
+
Configuration.default do |config|
|
74
|
+
config.csp = {
|
75
|
+
default_src: %w('self'),
|
76
|
+
report_only: false
|
112
77
|
}
|
113
|
-
|
114
|
-
csp = ContentSecurityPolicy.new(opts)
|
115
|
-
expect(csp.value).to match("report-uri http://lambda/result")
|
116
78
|
end
|
79
|
+
combined_config = CSP.combine_policies(Configuration.get.csp, report_only: true)
|
80
|
+
csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
|
81
|
+
expect(csp.name).to eq(CSP::REPORT_ONLY)
|
82
|
+
end
|
117
83
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
:
|
84
|
+
it "overrides the :block_all_mixed_content flag" do
|
85
|
+
Configuration.default do |config|
|
86
|
+
config.csp = {
|
87
|
+
default_src: %w(https:),
|
88
|
+
block_all_mixed_content: false
|
122
89
|
}
|
123
|
-
|
124
|
-
csp = ContentSecurityPolicy.new(opts)
|
125
|
-
expect(csp.value).to eq("default-src http://lambda/result; img-src http://lambda/result data:;")
|
126
|
-
expect(csp.name).to match("Content-Security-Policy")
|
127
90
|
end
|
91
|
+
combined_config = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true)
|
92
|
+
csp = ContentSecurityPolicy.new(combined_config)
|
93
|
+
expect(csp.value).to eq("default-src https:; block-all-mixed-content")
|
94
|
+
end
|
128
95
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
allow(controller).to receive(:current_user).and_return(user)
|
134
|
-
opts = {
|
135
|
-
:default_src => "'self'",
|
136
|
-
:enforce => lambda { |c| c.current_user.beta_testing? }
|
137
|
-
}
|
138
|
-
csp = ContentSecurityPolicy.new(opts, :controller => controller)
|
139
|
-
expect(csp.name).to match("Content-Security-Policy")
|
96
|
+
it "raises an error if appending to a OPT_OUT policy" do
|
97
|
+
Configuration.default do |config|
|
98
|
+
config.csp = OPT_OUT
|
140
99
|
end
|
100
|
+
expect do
|
101
|
+
CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com))
|
102
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
141
103
|
end
|
142
104
|
end
|
143
105
|
|
144
106
|
describe "#value" do
|
145
|
-
it "
|
146
|
-
|
147
|
-
|
148
|
-
expect(policy.name).to eq("Content-Security-Policy")
|
149
|
-
policy = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
|
150
|
-
expect(policy.name).to eq("Content-Security-Policy")
|
151
|
-
end
|
152
|
-
|
153
|
-
context "browser sniffing" do
|
154
|
-
let(:complex_opts) do
|
155
|
-
ALL_DIRECTIVES.inject({}) { |memo, directive| memo[directive] = "'self'"; memo }.merge(:block_all_mixed_content => '')
|
156
|
-
end
|
157
|
-
|
158
|
-
it "does not filter any directives for Chrome" do
|
159
|
-
policy = ContentSecurityPolicy.new(complex_opts, :request => request_for(CHROME))
|
160
|
-
expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content ; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';")
|
161
|
-
end
|
162
|
-
|
163
|
-
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
|
164
|
-
policy = ContentSecurityPolicy.new(complex_opts, :request => request_for(FIREFOX))
|
165
|
-
expect(policy.value).to eq("default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';")
|
166
|
-
end
|
167
|
-
|
168
|
-
it "filters base-uri, blocked-all-mixed-content, child-src, form-action, frame-ancestors, and plugin-types for safari" do
|
169
|
-
policy = ContentSecurityPolicy.new(complex_opts, :request => request_for(SAFARI))
|
170
|
-
expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self' data:; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self'; style-src 'self'; report-uri 'self';")
|
171
|
-
end
|
107
|
+
it "discards 'none' values if any other source expressions are present" do
|
108
|
+
csp = ContentSecurityPolicy.new(default_opts.merge(frame_src: %w('self' 'none')))
|
109
|
+
expect(csp.value).not_to include("'none'")
|
172
110
|
end
|
173
111
|
|
174
|
-
it "
|
175
|
-
csp = ContentSecurityPolicy.new(
|
176
|
-
expect
|
177
|
-
csp.value
|
178
|
-
}.to raise_error(RuntimeError)
|
112
|
+
it "discards source expressions besides unsafe-* expressions when * is present" do
|
113
|
+
csp = ContentSecurityPolicy.new(default_src: %w(* 'unsafe-inline' 'unsafe-eval' http: https: example.org))
|
114
|
+
expect(csp.value).to eq("default-src * 'unsafe-inline' 'unsafe-eval'")
|
179
115
|
end
|
180
116
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
it "appends the value if img-src is specified" do
|
188
|
-
csp = ContentSecurityPolicy.new({:default_src => "'self'", :img_src => "'self'"}, :request => request_for(CHROME))
|
189
|
-
expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
|
190
|
-
end
|
191
|
-
|
192
|
-
it "doesn't add a duplicate data uri if img-src specifies it already" do
|
193
|
-
csp = ContentSecurityPolicy.new({:default_src => "'self'", :img_src => "'self' data:"}, :request => request_for(CHROME))
|
194
|
-
expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
|
195
|
-
end
|
196
|
-
|
197
|
-
it "allows the user to disable img-src data: uris auto-whitelisting" do
|
198
|
-
csp = ContentSecurityPolicy.new({:default_src => "'self'", :img_src => "'self'", :disable_img_src_data_uri => true}, :request => request_for(CHROME))
|
199
|
-
expect(csp.value).to eq("default-src 'self'; img-src 'self';")
|
200
|
-
end
|
117
|
+
it "minifies source expressions based on overlapping wildcards" do
|
118
|
+
config = {
|
119
|
+
default_src: %w(a.example.org b.example.org *.example.org https://*.example.org)
|
120
|
+
}
|
121
|
+
csp = ContentSecurityPolicy.new(config)
|
122
|
+
expect(csp.value).to eq("default-src *.example.org")
|
201
123
|
end
|
202
124
|
|
203
|
-
it "
|
204
|
-
csp = ContentSecurityPolicy.new(
|
205
|
-
expect(csp.value).to
|
125
|
+
it "removes http/s schemes from hosts" do
|
126
|
+
csp = ContentSecurityPolicy.new(default_src: %w(https://example.org))
|
127
|
+
expect(csp.value).to eq("default-src example.org")
|
206
128
|
end
|
207
129
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
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;")
|
212
|
-
end
|
130
|
+
it "does not remove schemes from report-uri values" do
|
131
|
+
csp = ContentSecurityPolicy.new(default_src: %w(https:), report_uri: %w(https://example.org))
|
132
|
+
expect(csp.value).to eq("default-src https:; report-uri https://example.org")
|
213
133
|
end
|
214
134
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
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;")
|
219
|
-
end
|
135
|
+
it "removes nil from source lists" do
|
136
|
+
csp = ContentSecurityPolicy.new(default_src: ["https://example.org", nil])
|
137
|
+
expect(csp.value).to eq("default-src example.org")
|
220
138
|
end
|
221
139
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
226
|
-
end
|
227
|
-
|
228
|
-
it "adds a nonce and unsafe-inline to the script-src value when using firefox" do
|
229
|
-
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce"), :request => request_for(FIREFOX), :controller => controller)
|
230
|
-
expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
231
|
-
end
|
232
|
-
|
233
|
-
it "adds a nonce and unsafe-inline to the script-src value when using opera" do
|
234
|
-
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce"), :request => request_for(OPERA), :controller => controller)
|
235
|
-
expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
236
|
-
end
|
237
|
-
|
238
|
-
it "does not add a nonce and unsafe-inline to the script-src value when using Safari" do
|
239
|
-
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce"), :request => request_for(SAFARI), :controller => controller)
|
240
|
-
expect(header.value).to include("script-src 'self' 'unsafe-inline'")
|
241
|
-
expect(header.value).not_to include("nonce")
|
242
|
-
end
|
243
|
-
|
244
|
-
it "does not add a nonce and unsafe-inline to the script-src value when using IE" do
|
245
|
-
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce"), :request => request_for(IE), :controller => controller)
|
246
|
-
expect(header.value).to include("script-src 'self' 'unsafe-inline'")
|
247
|
-
expect(header.value).not_to include("nonce")
|
248
|
-
end
|
249
|
-
|
250
|
-
it "adds a nonce and unsafe-inline to the style-src value" do
|
251
|
-
header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "'self' nonce"), :request => request_for(CHROME), :controller => controller)
|
252
|
-
expect(header.value).to include("style-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
|
253
|
-
end
|
254
|
-
|
255
|
-
it "adds an identical nonce to the style and script-src directives" do
|
256
|
-
header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "'self' nonce", :script_src => "'self' nonce"), :request => request_for(CHROME), :controller => controller)
|
257
|
-
nonce = header.nonce
|
258
|
-
value = header.value
|
259
|
-
expect(value).to include("style-src 'self' 'nonce-#{nonce}' 'unsafe-inline'")
|
260
|
-
expect(value).to include("script-src 'self' 'nonce-#{nonce}' 'unsafe-inline'")
|
261
|
-
end
|
262
|
-
|
263
|
-
it "does not add 'unsafe-inline' twice" do
|
264
|
-
header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "'self' nonce 'unsafe-inline'"), :request => request_for(CHROME), :controller => controller)
|
265
|
-
expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline';")
|
266
|
-
end
|
140
|
+
it "deduplicates any source expressions" do
|
141
|
+
csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org example.org))
|
142
|
+
expect(csp.value).to eq("default-src example.org")
|
267
143
|
end
|
268
144
|
|
269
|
-
context "
|
270
|
-
let(:
|
271
|
-
|
272
|
-
:
|
273
|
-
|
274
|
-
:img_src => "http:"
|
275
|
-
}
|
276
|
-
})
|
277
|
-
}
|
278
|
-
|
279
|
-
it "adds directive values for headers on http" do
|
280
|
-
csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
|
281
|
-
expect(csp.value).to eq("default-src https:; frame-src http:; img-src https: data: http:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;")
|
145
|
+
context "browser sniffing" do
|
146
|
+
let (:complex_opts) do
|
147
|
+
ContentSecurityPolicy::ALL_DIRECTIVES.each_with_object({}) { |directive, hash| hash[directive] = %w('self') }
|
148
|
+
.merge(block_all_mixed_content: true, reflected_xss: "block")
|
149
|
+
.merge(script_src: %w('self'), script_nonce: 123456)
|
282
150
|
end
|
283
151
|
|
284
|
-
it "does not
|
285
|
-
|
286
|
-
expect(
|
152
|
+
it "does not filter any directives for Chrome" do
|
153
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
|
154
|
+
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'")
|
287
155
|
end
|
288
156
|
|
289
|
-
it "does not
|
290
|
-
|
291
|
-
expect(
|
157
|
+
it "does not filter any directives for Opera" do
|
158
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
|
159
|
+
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'")
|
292
160
|
end
|
293
|
-
end
|
294
161
|
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
double.tap do |env|
|
299
|
-
allow(env).to receive(:[]).with('HTTP_USER_AGENT').and_return(ua)
|
300
|
-
end
|
301
|
-
end
|
302
|
-
let(:request) do
|
303
|
-
double(
|
304
|
-
:ssl? => true,
|
305
|
-
:url => 'https://example.com',
|
306
|
-
:env => env
|
307
|
-
)
|
308
|
-
end
|
309
|
-
|
310
|
-
describe ".add_to_env" do
|
311
|
-
let(:controller) { double }
|
312
|
-
let(:config) { {:default_src => "'self'"} }
|
313
|
-
let(:options) { {:controller => controller} }
|
314
|
-
|
315
|
-
it "adds metadata to env" do
|
316
|
-
metadata = {
|
317
|
-
:config => config,
|
318
|
-
:options => options
|
319
|
-
}
|
320
|
-
expect(ContentSecurityPolicy).to receive(:options_from_request).and_return(options)
|
321
|
-
expect(env).to receive(:[]=).with(ContentSecurityPolicy::ENV_KEY, metadata)
|
322
|
-
ContentSecurityPolicy.add_to_env(request, controller, config)
|
323
|
-
end
|
162
|
+
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
|
163
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
|
164
|
+
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'")
|
324
165
|
end
|
325
166
|
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
expect(options).to eql({
|
330
|
-
:ua => ua,
|
331
|
-
:ssl => true,
|
332
|
-
:request_uri => 'https://example.com'
|
333
|
-
})
|
334
|
-
end
|
167
|
+
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
|
168
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
|
169
|
+
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'")
|
335
170
|
end
|
336
171
|
end
|
337
172
|
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
|
@@ -2,34 +2,27 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
module SecureHeaders
|
4
4
|
describe XContentTypeOptions do
|
5
|
-
specify{ expect(XContentTypeOptions.new.name).to eq("X-Content-Type-Options") }
|
6
|
-
|
7
5
|
describe "#value" do
|
8
|
-
specify { expect(XContentTypeOptions.
|
9
|
-
specify { expect(XContentTypeOptions.
|
10
|
-
specify { expect(XContentTypeOptions.new(:value => 'nosniff').value).to eq("nosniff")}
|
6
|
+
specify { expect(XContentTypeOptions.make_header).to eq([XContentTypeOptions::HEADER_NAME, XContentTypeOptions::DEFAULT_VALUE]) }
|
7
|
+
specify { expect(XContentTypeOptions.make_header("nosniff")).to eq([XContentTypeOptions::HEADER_NAME, "nosniff"]) }
|
11
8
|
|
12
9
|
context "invalid configuration values" do
|
13
10
|
it "accepts nosniff" do
|
14
|
-
expect
|
15
|
-
XContentTypeOptions.
|
16
|
-
|
17
|
-
|
18
|
-
expect {
|
19
|
-
XContentTypeOptions.new(:value => "nosniff")
|
20
|
-
}.not_to raise_error
|
11
|
+
expect do
|
12
|
+
XContentTypeOptions.validate_config!("nosniff")
|
13
|
+
end.not_to raise_error
|
21
14
|
end
|
22
15
|
|
23
16
|
it "accepts nil" do
|
24
|
-
expect
|
25
|
-
XContentTypeOptions.
|
26
|
-
|
17
|
+
expect do
|
18
|
+
XContentTypeOptions.validate_config!(nil)
|
19
|
+
end.not_to raise_error
|
27
20
|
end
|
28
21
|
|
29
22
|
it "doesn't accept anything besides no-sniff" do
|
30
|
-
expect
|
31
|
-
XContentTypeOptions.
|
32
|
-
|
23
|
+
expect do
|
24
|
+
XContentTypeOptions.validate_config!("donkey")
|
25
|
+
end.to raise_error(XContentTypeOptionsConfigError)
|
33
26
|
end
|
34
27
|
end
|
35
28
|
end
|