secure_headers 2.2.4 → 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 -0
- data/CHANGELOG.md +205 -0
- data/Gemfile +9 -10
- data/Guardfile +12 -0
- data/README.md +172 -274
- data/Rakefile +2 -36
- data/lib/secure_headers/configuration.rb +189 -0
- data/lib/secure_headers/headers/content_security_policy.rb +362 -211
- 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 +245 -150
- 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 +175 -262
- 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 -28
- data/upgrading-to-3-0.md +37 -0
- metadata +14 -94
- 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 -20
- 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 -56
- 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
|
@@ -1,314 +1,240 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
subject.set_hsts_header
|
|
42
|
-
subject.set_x_frame_options_header
|
|
43
|
-
subject.set_x_content_type_options_header
|
|
44
|
-
subject.set_x_xss_protection_header
|
|
45
|
-
subject.set_x_download_options_header
|
|
46
|
-
subject.set_x_permitted_cross_domain_policies_header
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
describe "#set_header" do
|
|
50
|
-
it "accepts name/value pairs" do
|
|
51
|
-
should_assign_header("X-Hipster-Ipsum", "kombucha")
|
|
52
|
-
subject.send(:set_header, "X-Hipster-Ipsum", "kombucha")
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
it "accepts header objects" do
|
|
56
|
-
should_assign_header("Strict-Transport-Security", SecureHeaders::StrictTransportSecurity::Constants::DEFAULT_VALUE)
|
|
57
|
-
subject.send(:set_header, SecureHeaders::StrictTransportSecurity.new)
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
describe "#set_security_headers" do
|
|
62
|
-
USER_AGENTS.each do |name, useragent|
|
|
63
|
-
it "sets all default headers for #{name} (smoke test)" do
|
|
64
|
-
stub_user_agent(useragent)
|
|
65
|
-
number_of_headers = 7
|
|
66
|
-
expect(subject).to receive(:set_header).exactly(number_of_headers).times # a request for a given header
|
|
67
|
-
subject.set_csp_header
|
|
68
|
-
subject.set_x_frame_options_header
|
|
69
|
-
subject.set_hsts_header
|
|
70
|
-
subject.set_hpkp_header
|
|
71
|
-
subject.set_x_xss_protection_header
|
|
72
|
-
subject.set_x_content_type_options_header
|
|
73
|
-
subject.set_x_download_options_header
|
|
74
|
-
subject.set_x_permitted_cross_domain_policies_header
|
|
3
|
+
module SecureHeaders
|
|
4
|
+
describe SecureHeaders do
|
|
5
|
+
example_hpkp_config = {
|
|
6
|
+
max_age: 1_000_000,
|
|
7
|
+
include_subdomains: true,
|
|
8
|
+
report_uri: '//example.com/uri-directive',
|
|
9
|
+
pins: [
|
|
10
|
+
{ sha256: 'abc' },
|
|
11
|
+
{ sha256: '123' }
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
example_hpkp_config_value = %(max-age=1000000; pin-sha256="abc"; pin-sha256="123"; report-uri="//example.com/uri-directive"; includeSubDomains)
|
|
16
|
+
|
|
17
|
+
before(:each) do
|
|
18
|
+
reset_config
|
|
19
|
+
@request = Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "raises a NotYetConfiguredError if default has not been set" do
|
|
23
|
+
expect do
|
|
24
|
+
SecureHeaders.header_hash_for(@request)
|
|
25
|
+
end.to raise_error(Configuration::NotYetConfiguredError)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do
|
|
29
|
+
expect do
|
|
30
|
+
SecureHeaders.opt_out_of_header(@request, CSP::CONFIG_KEY)
|
|
31
|
+
end.to raise_error(Configuration::NotYetConfiguredError)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe "#header_hash_for" do
|
|
35
|
+
it "allows you to opt out of individual headers" do
|
|
36
|
+
Configuration.default
|
|
37
|
+
SecureHeaders.opt_out_of_header(@request, CSP::CONFIG_KEY)
|
|
38
|
+
hash = SecureHeaders.header_hash_for(@request)
|
|
39
|
+
expect(hash['Content-Security-Policy-Report-Only']).to be_nil
|
|
40
|
+
expect(hash['Content-Security-Policy']).to be_nil
|
|
75
41
|
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
it "does not set the X-Content-Type-Options header if disabled" do
|
|
79
|
-
stub_user_agent(USER_AGENTS[:ie])
|
|
80
|
-
should_not_assign_header(X_CONTENT_TYPE_OPTIONS_HEADER_NAME)
|
|
81
|
-
subject.set_x_content_type_options_header(false)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
it "does not set the X-XSS-Protection header if disabled" do
|
|
85
|
-
should_not_assign_header(X_XSS_PROTECTION_HEADER_NAME)
|
|
86
|
-
subject.set_x_xss_protection_header(false)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
it "does not set the X-Download-Options header if disabled" do
|
|
90
|
-
should_not_assign_header(XDO_HEADER_NAME)
|
|
91
|
-
subject.set_x_download_options_header(false)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
it "does not set the X-Frame-Options header if disabled" do
|
|
95
|
-
should_not_assign_header(XFO_HEADER_NAME)
|
|
96
|
-
subject.set_x_frame_options_header(false)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
it "does not set the X-Permitted-Cross-Domain-Policies header if disabled" do
|
|
100
|
-
should_not_assign_header(XPCDP_HEADER_NAME)
|
|
101
|
-
subject.set_x_permitted_cross_domain_policies_header(false)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
it "does not set the HSTS header if disabled" do
|
|
105
|
-
should_not_assign_header(HSTS_HEADER_NAME)
|
|
106
|
-
subject.set_hsts_header(false)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
it "does not set the HSTS header if request is over HTTP" do
|
|
110
|
-
allow(subject).to receive_message_chain(:request, :ssl?).and_return(false)
|
|
111
|
-
should_not_assign_header(HSTS_HEADER_NAME)
|
|
112
|
-
subject.set_hsts_header({:include_subdomains => true})
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
it "does not set the HPKP header if disabled" do
|
|
116
|
-
should_not_assign_header(HPKP_HEADER_NAME)
|
|
117
|
-
subject.set_hpkp_header
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
it "does not set the HPKP header if request is over HTTP" do
|
|
121
|
-
allow(subject).to receive_message_chain(:request, :ssl?).and_return(false)
|
|
122
|
-
should_not_assign_header(HPKP_HEADER_NAME)
|
|
123
|
-
subject.set_hpkp_header(:max_age => 1234)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
it "does not set the CSP header if disabled" do
|
|
127
|
-
stub_user_agent(USER_AGENTS[:chrome])
|
|
128
|
-
should_not_assign_header(HEADER_NAME)
|
|
129
|
-
subject.set_csp_header(false)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
it "saves the options to the env when using script hashes" do
|
|
133
|
-
opts = {
|
|
134
|
-
:default_src => 'self',
|
|
135
|
-
:script_hash_middleware => true
|
|
136
|
-
}
|
|
137
|
-
stub_user_agent(USER_AGENTS[:chrome])
|
|
138
|
-
|
|
139
|
-
expect(SecureHeaders::ContentSecurityPolicy).to receive(:add_to_env)
|
|
140
|
-
subject.set_csp_header(opts)
|
|
141
|
-
end
|
|
142
42
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
config.x_content_type_options = false
|
|
150
|
-
config.x_xss_protection = false
|
|
151
|
-
config.csp = false
|
|
152
|
-
config.x_download_options = false
|
|
153
|
-
config.x_permitted_cross_domain_policies = false
|
|
43
|
+
it "allows you to opt out entirely" do
|
|
44
|
+
Configuration.default
|
|
45
|
+
SecureHeaders.opt_out_of_all_protection(@request)
|
|
46
|
+
hash = SecureHeaders.header_hash_for(@request)
|
|
47
|
+
ALL_HEADER_CLASSES.each do |klass|
|
|
48
|
+
expect(hash[klass::CONFIG_KEY]).to be_nil
|
|
154
49
|
end
|
|
155
|
-
expect(subject).not_to receive(:set_header)
|
|
156
|
-
set_security_headers(subject)
|
|
157
|
-
reset_config
|
|
158
50
|
end
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
51
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
it "allows a custom X-Frame-Options header" do
|
|
169
|
-
should_assign_header(XFO_HEADER_NAME, "DENY")
|
|
170
|
-
subject.set_x_frame_options_header(:value => 'DENY')
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
describe "#set_x_download_options_header" do
|
|
175
|
-
it "sets the X-Download-Options header" do
|
|
176
|
-
should_assign_header(XDO_HEADER_NAME, SecureHeaders::XDownloadOptions::Constants::DEFAULT_VALUE)
|
|
177
|
-
subject.set_x_download_options_header
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
it "allows a custom X-Download-Options header" do
|
|
181
|
-
should_assign_header(XDO_HEADER_NAME, "noopen")
|
|
182
|
-
subject.set_x_download_options_header(:value => 'noopen')
|
|
183
|
-
end
|
|
184
|
-
end
|
|
52
|
+
it "allows you to override X-Frame-Options settings" do
|
|
53
|
+
Configuration.default
|
|
54
|
+
SecureHeaders.override_x_frame_options(@request, XFrameOptions::DENY)
|
|
55
|
+
hash = SecureHeaders.header_hash_for(@request)
|
|
56
|
+
expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::DENY)
|
|
57
|
+
end
|
|
185
58
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
59
|
+
it "allows you to override opting out" do
|
|
60
|
+
Configuration.default do |config|
|
|
61
|
+
config.x_frame_options = OPT_OUT
|
|
62
|
+
config.csp = OPT_OUT
|
|
63
|
+
end
|
|
191
64
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
subject.set_hsts_header(:max_age => 1234)
|
|
195
|
-
end
|
|
65
|
+
SecureHeaders.override_x_frame_options(@request, XFrameOptions::SAMEORIGIN)
|
|
66
|
+
SecureHeaders.override_content_security_policy_directives(@request, default_src: %w(https:), script_src: %w('self'))
|
|
196
67
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
68
|
+
hash = SecureHeaders.header_hash_for(@request)
|
|
69
|
+
expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
|
|
70
|
+
expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
|
|
71
|
+
end
|
|
201
72
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
73
|
+
it "produces a hash of headers with default config" do
|
|
74
|
+
Configuration.default
|
|
75
|
+
hash = SecureHeaders.header_hash_for(@request)
|
|
76
|
+
expect_default_values(hash)
|
|
77
|
+
end
|
|
207
78
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
79
|
+
it "does not set the HSTS header if request is over HTTP" do
|
|
80
|
+
plaintext_request = Rack::Request.new({})
|
|
81
|
+
Configuration.default do |config|
|
|
82
|
+
config.hsts = "max-age=123456"
|
|
83
|
+
end
|
|
84
|
+
expect(SecureHeaders.header_hash_for(plaintext_request)[StrictTransportSecurity::HEADER_NAME]).to be_nil
|
|
85
|
+
end
|
|
213
86
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
87
|
+
it "does not set the HPKP header if request is over HTTP" do
|
|
88
|
+
plaintext_request = Rack::Request.new({})
|
|
89
|
+
Configuration.default do |config|
|
|
90
|
+
config.hpkp = example_hpkp_config
|
|
91
|
+
end
|
|
218
92
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
subject.set_hpkp_header(:max_age => 1234)
|
|
222
|
-
end
|
|
93
|
+
expect(SecureHeaders.header_hash_for(plaintext_request)[PublicKeyPins::HEADER_NAME]).to be_nil
|
|
94
|
+
end
|
|
223
95
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
96
|
+
context "content security policy" do
|
|
97
|
+
it "appends a value to csp directive" do
|
|
98
|
+
Configuration.default do |config|
|
|
99
|
+
config.csp = {
|
|
100
|
+
default_src: %w('self'),
|
|
101
|
+
script_src: %w(mycdn.com 'unsafe-inline')
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
SecureHeaders.append_content_security_policy_directives(@request, script_src: %w(anothercdn.com))
|
|
106
|
+
hash = SecureHeaders.header_hash_for(@request)
|
|
107
|
+
expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
|
|
108
|
+
end
|
|
228
109
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
110
|
+
it "overrides individual directives" do
|
|
111
|
+
Configuration.default do |config|
|
|
112
|
+
config.csp = {
|
|
113
|
+
default_src: %w('self')
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
SecureHeaders.override_content_security_policy_directives(@request, default_src: %w('none'))
|
|
117
|
+
hash = SecureHeaders.header_hash_for(@request)
|
|
118
|
+
expect(hash[CSP::HEADER_NAME]).to eq("default-src 'none'")
|
|
119
|
+
end
|
|
233
120
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
121
|
+
it "overrides non-existant directives" do
|
|
122
|
+
Configuration.default
|
|
123
|
+
SecureHeaders.override_content_security_policy_directives(@request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL])
|
|
124
|
+
hash = SecureHeaders.header_hash_for(@request)
|
|
125
|
+
expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; img-src data:")
|
|
126
|
+
end
|
|
239
127
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
128
|
+
it "does not append a nonce when the browser does not support it" do
|
|
129
|
+
Configuration.default do |config|
|
|
130
|
+
config.csp = {
|
|
131
|
+
default_src: %w('self'),
|
|
132
|
+
script_src: %w(mycdn.com 'unsafe-inline'),
|
|
133
|
+
style_src: %w('self')
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
request = Rack::Request.new(@request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
|
|
138
|
+
nonce = SecureHeaders.content_security_policy_script_nonce(request)
|
|
139
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
140
|
+
expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
|
|
141
|
+
end
|
|
245
142
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
143
|
+
it "appends a nonce to the script-src when used" do
|
|
144
|
+
Configuration.default do |config|
|
|
145
|
+
config.csp = {
|
|
146
|
+
default_src: %w('self'),
|
|
147
|
+
script_src: %w(mycdn.com),
|
|
148
|
+
style_src: %w('self')
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
request = Rack::Request.new(@request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
|
|
153
|
+
nonce = SecureHeaders.content_security_policy_script_nonce(request)
|
|
154
|
+
|
|
155
|
+
# simulate the nonce being used multiple times in a request:
|
|
156
|
+
SecureHeaders.content_security_policy_script_nonce(request)
|
|
157
|
+
SecureHeaders.content_security_policy_script_nonce(request)
|
|
158
|
+
SecureHeaders.content_security_policy_script_nonce(request)
|
|
159
|
+
|
|
160
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
161
|
+
expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'")
|
|
162
|
+
end
|
|
163
|
+
end
|
|
249
164
|
end
|
|
250
165
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
166
|
+
context "validation" do
|
|
167
|
+
it "validates your hsts config upon configuration" do
|
|
168
|
+
expect do
|
|
169
|
+
Configuration.default do |config|
|
|
170
|
+
config.hsts = 'lol'
|
|
171
|
+
end
|
|
172
|
+
end.to raise_error(STSConfigError)
|
|
173
|
+
end
|
|
256
174
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
end
|
|
175
|
+
it "validates your csp config upon configuration" do
|
|
176
|
+
expect do
|
|
177
|
+
Configuration.default do |config|
|
|
178
|
+
config.csp = { CSP::DEFAULT_SRC => '123456' }
|
|
179
|
+
end
|
|
180
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
181
|
+
end
|
|
263
182
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
183
|
+
it "raises errors for unknown directives" do
|
|
184
|
+
expect do
|
|
185
|
+
Configuration.default do |config|
|
|
186
|
+
config.csp = { made_up_directive: '123456' }
|
|
187
|
+
end
|
|
188
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
189
|
+
end
|
|
268
190
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
191
|
+
it "validates your xfo config upon configuration" do
|
|
192
|
+
expect do
|
|
193
|
+
Configuration.default do |config|
|
|
194
|
+
config.x_frame_options = "NOPE"
|
|
195
|
+
end
|
|
196
|
+
end.to raise_error(XFOConfigError)
|
|
273
197
|
end
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
198
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
199
|
+
it "validates your xcto config upon configuration" do
|
|
200
|
+
expect do
|
|
201
|
+
Configuration.default do |config|
|
|
202
|
+
config.x_content_type_options = "lol"
|
|
203
|
+
end
|
|
204
|
+
end.to raise_error(XContentTypeOptionsConfigError)
|
|
283
205
|
end
|
|
284
|
-
end
|
|
285
206
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
207
|
+
it "validates your x_xss config upon configuration" do
|
|
208
|
+
expect do
|
|
209
|
+
Configuration.default do |config|
|
|
210
|
+
config.x_xss_protection = "lol"
|
|
211
|
+
end
|
|
212
|
+
end.to raise_error(XXssProtectionConfigError)
|
|
291
213
|
end
|
|
292
|
-
end
|
|
293
214
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
215
|
+
it "validates your xdo config upon configuration" do
|
|
216
|
+
expect do
|
|
217
|
+
Configuration.default do |config|
|
|
218
|
+
config.x_download_options = "lol"
|
|
219
|
+
end
|
|
220
|
+
end.to raise_error(XDOConfigError)
|
|
299
221
|
end
|
|
300
|
-
end
|
|
301
|
-
end
|
|
302
222
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
223
|
+
it "validates your x_permitted_cross_domain_policies config upon configuration" do
|
|
224
|
+
expect do
|
|
225
|
+
Configuration.default do |config|
|
|
226
|
+
config.x_permitted_cross_domain_policies = "lol"
|
|
227
|
+
end
|
|
228
|
+
end.to raise_error(XPCDPConfigError)
|
|
229
|
+
end
|
|
308
230
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
231
|
+
it "validates your hpkp config upon configuration" do
|
|
232
|
+
expect do
|
|
233
|
+
Configuration.default do |config|
|
|
234
|
+
config.hpkp = "lol"
|
|
235
|
+
end
|
|
236
|
+
end.to raise_error(PublicKeyPinsConfigError)
|
|
237
|
+
end
|
|
312
238
|
end
|
|
313
239
|
end
|
|
314
240
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,40 +1,44 @@
|
|
|
1
1
|
require 'rubygems'
|
|
2
2
|
require 'rspec'
|
|
3
|
+
require 'rack'
|
|
4
|
+
require 'pry-nav'
|
|
3
5
|
|
|
4
6
|
require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers')
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
require 'coveralls'
|
|
8
|
-
Coveralls.wear!
|
|
9
|
-
rescue LoadError
|
|
10
|
-
# damn you 1.8.7
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
include ::SecureHeaders::PublicKeyPins::Constants
|
|
14
|
-
include ::SecureHeaders::StrictTransportSecurity::Constants
|
|
15
|
-
include ::SecureHeaders::ContentSecurityPolicy::Constants
|
|
16
|
-
include ::SecureHeaders::XFrameOptions::Constants
|
|
17
|
-
include ::SecureHeaders::XXssProtection::Constants
|
|
18
|
-
include ::SecureHeaders::XContentTypeOptions::Constants
|
|
19
|
-
include ::SecureHeaders::XDownloadOptions::Constants
|
|
20
|
-
include ::SecureHeaders::XPermittedCrossDomainPolicies::Constants
|
|
8
|
+
ENV["RAILS_ENV"] = "test"
|
|
21
9
|
|
|
22
10
|
USER_AGENTS = {
|
|
23
|
-
:
|
|
24
|
-
:
|
|
25
|
-
:
|
|
26
|
-
:
|
|
27
|
-
:
|
|
28
|
-
:
|
|
29
|
-
:
|
|
30
|
-
:
|
|
31
|
-
:
|
|
11
|
+
firefox: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1',
|
|
12
|
+
chrome: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.56 Safari/536.5',
|
|
13
|
+
ie: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)',
|
|
14
|
+
opera: 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00',
|
|
15
|
+
ios5: "Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3",
|
|
16
|
+
ios6: "Mozilla/5.0 (iPhone; CPU iPhone OS 614 like Mac OS X) AppleWebKit/536.26 (KHTML like Gecko) Version/6.0 Mobile/10B350 Safari/8536.25",
|
|
17
|
+
safari5: "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko ) Version/5.1 Mobile/9B176 Safari/7534.48.3",
|
|
18
|
+
safari5_1: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10",
|
|
19
|
+
safari6: "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1"
|
|
32
20
|
}
|
|
33
21
|
|
|
34
|
-
def
|
|
35
|
-
expect(
|
|
22
|
+
def expect_default_values(hash)
|
|
23
|
+
expect(hash[SecureHeaders::CSP::HEADER_NAME]).to eq(SecureHeaders::CSP::DEFAULT_VALUE)
|
|
24
|
+
expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE)
|
|
25
|
+
expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE)
|
|
26
|
+
expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE)
|
|
27
|
+
expect(hash[SecureHeaders::XXssProtection::HEADER_NAME]).to eq(SecureHeaders::XXssProtection::DEFAULT_VALUE)
|
|
28
|
+
expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE)
|
|
29
|
+
expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module SecureHeaders
|
|
33
|
+
class Configuration
|
|
34
|
+
class << self
|
|
35
|
+
def clear_configurations
|
|
36
|
+
@configurations = nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
36
40
|
end
|
|
37
41
|
|
|
38
|
-
def
|
|
39
|
-
|
|
42
|
+
def reset_config
|
|
43
|
+
SecureHeaders::Configuration.clear_configurations
|
|
40
44
|
end
|
data/upgrading-to-3-0.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
`secure_headers` 3.0 is a near-complete rewrite. It includes breaking changes and removes a lot of features that were either leftover from the days when the CSP standard was not fully adopted or were just downright confusing.
|
|
2
|
+
|
|
3
|
+
Changes
|
|
4
|
+
==
|
|
5
|
+
|
|
6
|
+
| What | < = 2.x | >= 3.0 |
|
|
7
|
+
|----------------------------------|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
8
|
+
| Global configuration | `SecureHeaders::Configuration.configure` block | `SecureHeaders::Configuration.default` block |
|
|
9
|
+
| All headers besides HPKP and CSP | Accept hashes as config values | Must be strings (validated during configuration) |
|
|
10
|
+
| CSP directive values | Accepted space delimited strings OR arrays of strings | Must be arrays of strings |
|
|
11
|
+
| `self`/`none` source expressions | could be `self` / `none` / `'self'` / `'none'` | Must be `'self'` or `'none'` |
|
|
12
|
+
| `inline` / `eval` source expressions | could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` |
|
|
13
|
+
| Per-action configuration | override [`def secure_header_options_for(header, options)`](https://github.com/twitter/secureheaders/commit/bb9ebc6c12a677aad29af8e0f08ffd1def56efec#diff-04c6e90faac2675aa89e2176d2eec7d8R111) | Use [named overrides](https://github.com/twitter/secureheaders#named-overrides) or [per-action helpers](https://github.com/twitter/secureheaders#per-action-configuration) |
|
|
14
|
+
| CSP/HPKP use `report_only` config that defaults to false | `enforce: false` | `report_only: false` |
|
|
15
|
+
|
|
16
|
+
Migrating to 3.x from <= 2.x
|
|
17
|
+
==
|
|
18
|
+
|
|
19
|
+
1. Convert all headers except for CSP/HPKP using hashes to string values. The values are validated at runtime and will provide guidance on misconfigured headers.
|
|
20
|
+
1. Convert all instances of `self`/`none`/`eval`/`inline` to the corresponding values in the above table.
|
|
21
|
+
1. Convert all CSP space-delimited directives to an array of strings.
|
|
22
|
+
1. Convert all `enforce: true|false` to `report_only: true|false`.
|
|
23
|
+
|
|
24
|
+
Everything is terrible, why should I upgrade?
|
|
25
|
+
==
|
|
26
|
+
|
|
27
|
+
`secure_headers` <= 2.x built every header per request using a series of automatically included `before_filters`. This is horribly inefficient because:
|
|
28
|
+
|
|
29
|
+
1. `before_filters` are slow and adding 8 per request isn't great
|
|
30
|
+
1. We are rebuilding strings that may never change for every request
|
|
31
|
+
1. Errors in the request may mean that the headers never get set in the first place
|
|
32
|
+
|
|
33
|
+
`secure_headers` 3.x sets headers in rack middleware that runs once per request and uses configuration values passed via `request.env`. This is much more efficient and somewhat guarantees that headers will always be set. **The values for the headers are cached and reused per request**.
|
|
34
|
+
|
|
35
|
+
Also, there is a more flexible API for customizing content security policies / X-Frame-Options. In practice, none of the other headers need granular controls. One way of customizing headers per request is to use the helper methods. The only downside of this technique is that headers will be computed from scratch.
|
|
36
|
+
|
|
37
|
+
See the [README](README.md) for more information.
|