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.
Files changed (127) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +3 -1
  5. data/CHANGELOG.md +205 -0
  6. data/Gemfile +9 -10
  7. data/Guardfile +12 -0
  8. data/README.md +175 -276
  9. data/Rakefile +2 -36
  10. data/lib/secure_headers/configuration.rb +189 -0
  11. data/lib/secure_headers/headers/content_security_policy.rb +367 -188
  12. data/lib/secure_headers/headers/public_key_pins.rb +43 -57
  13. data/lib/secure_headers/headers/strict_transport_security.rb +21 -47
  14. data/lib/secure_headers/headers/x_content_type_options.rb +19 -32
  15. data/lib/secure_headers/headers/x_download_options.rb +18 -31
  16. data/lib/secure_headers/headers/x_frame_options.rb +24 -32
  17. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +19 -32
  18. data/lib/secure_headers/headers/x_xss_protection.rb +17 -46
  19. data/lib/secure_headers/middleware.rb +15 -0
  20. data/lib/secure_headers/padrino.rb +1 -2
  21. data/lib/secure_headers/railtie.rb +9 -6
  22. data/lib/secure_headers/view_helper.rb +27 -44
  23. data/lib/secure_headers.rb +244 -145
  24. data/secure_headers.gemspec +7 -11
  25. data/spec/lib/secure_headers/configuration_spec.rb +80 -0
  26. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +176 -213
  27. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +17 -17
  28. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +11 -43
  29. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +13 -18
  30. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +13 -17
  31. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +15 -17
  32. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +22 -39
  33. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +22 -30
  34. data/spec/lib/secure_headers/middleware_spec.rb +40 -0
  35. data/spec/lib/secure_headers_spec.rb +204 -278
  36. data/spec/spec_helper.rb +32 -25
  37. data/upgrading-to-3-0.md +37 -0
  38. metadata +28 -95
  39. data/fixtures/rails_3_2_12/.rspec +0 -1
  40. data/fixtures/rails_3_2_12/Gemfile +0 -6
  41. data/fixtures/rails_3_2_12/README.rdoc +0 -261
  42. data/fixtures/rails_3_2_12/Rakefile +0 -7
  43. data/fixtures/rails_3_2_12/app/controllers/application_controller.rb +0 -4
  44. data/fixtures/rails_3_2_12/app/controllers/other_things_controller.rb +0 -5
  45. data/fixtures/rails_3_2_12/app/controllers/things_controller.rb +0 -5
  46. data/fixtures/rails_3_2_12/app/models/.gitkeep +0 -0
  47. data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +0 -11
  48. data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +0 -2
  49. data/fixtures/rails_3_2_12/app/views/things/index.html.erb +0 -1
  50. data/fixtures/rails_3_2_12/config/application.rb +0 -14
  51. data/fixtures/rails_3_2_12/config/boot.rb +0 -6
  52. data/fixtures/rails_3_2_12/config/environment.rb +0 -5
  53. data/fixtures/rails_3_2_12/config/environments/test.rb +0 -37
  54. data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +0 -17
  55. data/fixtures/rails_3_2_12/config/routes.rb +0 -4
  56. data/fixtures/rails_3_2_12/config/script_hashes.yml +0 -5
  57. data/fixtures/rails_3_2_12/config.ru +0 -7
  58. data/fixtures/rails_3_2_12/lib/assets/.gitkeep +0 -0
  59. data/fixtures/rails_3_2_12/lib/tasks/.gitkeep +0 -0
  60. data/fixtures/rails_3_2_12/log/.gitkeep +0 -0
  61. data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +0 -83
  62. data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +0 -54
  63. data/fixtures/rails_3_2_12/spec/spec_helper.rb +0 -15
  64. data/fixtures/rails_3_2_12/vendor/assets/javascripts/.gitkeep +0 -0
  65. data/fixtures/rails_3_2_12/vendor/assets/stylesheets/.gitkeep +0 -0
  66. data/fixtures/rails_3_2_12/vendor/plugins/.gitkeep +0 -0
  67. data/fixtures/rails_3_2_12_no_init/.rspec +0 -1
  68. data/fixtures/rails_3_2_12_no_init/Gemfile +0 -5
  69. data/fixtures/rails_3_2_12_no_init/README.rdoc +0 -261
  70. data/fixtures/rails_3_2_12_no_init/Rakefile +0 -7
  71. data/fixtures/rails_3_2_12_no_init/app/controllers/application_controller.rb +0 -4
  72. data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +0 -6
  73. data/fixtures/rails_3_2_12_no_init/app/controllers/things_controller.rb +0 -5
  74. data/fixtures/rails_3_2_12_no_init/app/models/.gitkeep +0 -0
  75. data/fixtures/rails_3_2_12_no_init/app/views/layouts/application.html.erb +0 -12
  76. data/fixtures/rails_3_2_12_no_init/app/views/other_things/index.html.erb +0 -1
  77. data/fixtures/rails_3_2_12_no_init/app/views/things/index.html.erb +0 -0
  78. data/fixtures/rails_3_2_12_no_init/config/application.rb +0 -17
  79. data/fixtures/rails_3_2_12_no_init/config/boot.rb +0 -6
  80. data/fixtures/rails_3_2_12_no_init/config/environment.rb +0 -5
  81. data/fixtures/rails_3_2_12_no_init/config/environments/test.rb +0 -37
  82. data/fixtures/rails_3_2_12_no_init/config/routes.rb +0 -4
  83. data/fixtures/rails_3_2_12_no_init/config.ru +0 -4
  84. data/fixtures/rails_3_2_12_no_init/lib/assets/.gitkeep +0 -0
  85. data/fixtures/rails_3_2_12_no_init/lib/tasks/.gitkeep +0 -0
  86. data/fixtures/rails_3_2_12_no_init/log/.gitkeep +0 -0
  87. data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +0 -50
  88. data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +0 -54
  89. data/fixtures/rails_3_2_12_no_init/spec/spec_helper.rb +0 -5
  90. data/fixtures/rails_3_2_12_no_init/vendor/assets/javascripts/.gitkeep +0 -0
  91. data/fixtures/rails_3_2_12_no_init/vendor/assets/stylesheets/.gitkeep +0 -0
  92. data/fixtures/rails_3_2_12_no_init/vendor/plugins/.gitkeep +0 -0
  93. data/fixtures/rails_4_1_8/Gemfile +0 -5
  94. data/fixtures/rails_4_1_8/README.rdoc +0 -28
  95. data/fixtures/rails_4_1_8/Rakefile +0 -6
  96. data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +0 -4
  97. data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
  98. data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +0 -5
  99. data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +0 -5
  100. data/fixtures/rails_4_1_8/app/models/.keep +0 -0
  101. data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
  102. data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +0 -11
  103. data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +0 -2
  104. data/fixtures/rails_4_1_8/app/views/things/index.html.erb +0 -1
  105. data/fixtures/rails_4_1_8/config/application.rb +0 -15
  106. data/fixtures/rails_4_1_8/config/boot.rb +0 -4
  107. data/fixtures/rails_4_1_8/config/environment.rb +0 -5
  108. data/fixtures/rails_4_1_8/config/environments/test.rb +0 -10
  109. data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +0 -17
  110. data/fixtures/rails_4_1_8/config/routes.rb +0 -4
  111. data/fixtures/rails_4_1_8/config/script_hashes.yml +0 -5
  112. data/fixtures/rails_4_1_8/config/secrets.yml +0 -22
  113. data/fixtures/rails_4_1_8/config.ru +0 -4
  114. data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
  115. data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
  116. data/fixtures/rails_4_1_8/log/.keep +0 -0
  117. data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +0 -83
  118. data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +0 -59
  119. data/fixtures/rails_4_1_8/spec/spec_helper.rb +0 -15
  120. data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
  121. data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
  122. data/lib/secure_headers/hash_helper.rb +0 -7
  123. data/lib/secure_headers/header.rb +0 -5
  124. data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +0 -22
  125. data/lib/secure_headers/version.rb +0 -3
  126. data/lib/tasks/tasks.rake +0 -48
  127. 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
- describe SecureHeaders do
4
- class DummyClass
5
- include ::SecureHeaders
6
- end
7
-
8
- subject {DummyClass.new}
9
- let(:headers) {double}
10
- let(:response) {double(:headers => headers)}
11
- let(:max_age) {99}
12
- let(:request) {double(:ssl? => true, :url => 'https://example.com')}
13
-
14
- before(:each) do
15
- stub_user_agent(nil)
16
- allow(headers).to receive(:[])
17
- allow(subject).to receive(:response).and_return(response)
18
- allow(subject).to receive(:request).and_return(request)
19
- end
20
-
21
- def stub_user_agent val
22
- allow(request).to receive_message_chain(:env, :[]).and_return(val)
23
- end
24
-
25
- def reset_config
26
- ::SecureHeaders::Configuration.configure do |config|
27
- config.hpkp = nil
28
- config.hsts = nil
29
- config.x_frame_options = nil
30
- config.x_content_type_options = nil
31
- config.x_xss_protection = nil
32
- config.csp = nil
33
- config.x_download_options = nil
34
- config.x_permitted_cross_domain_policies = nil
35
- end
36
- end
37
-
38
- def set_security_headers(subject)
39
- subject.set_csp_header
40
- subject.set_hpkp_header
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
- context "when disabled by configuration settings" do
144
- it "does not set any headers when disabled" do
145
- ::SecureHeaders::Configuration.configure do |config|
146
- config.hsts = false
147
- config.hpkp = false
148
- config.x_frame_options = false
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
- describe "#set_x_frame_options_header" do
163
- it "sets the X-Frame-Options header" do
164
- should_assign_header(XFO_HEADER_NAME, SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE)
165
- subject.set_x_frame_options_header
166
- end
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
- describe "#set_strict_transport_security" do
187
- it "sets the Strict-Transport-Security header" do
188
- should_assign_header(HSTS_HEADER_NAME, SecureHeaders::StrictTransportSecurity::Constants::DEFAULT_VALUE)
189
- subject.set_hsts_header
190
- end
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
- it "allows you to specific a custom max-age value" do
193
- should_assign_header(HSTS_HEADER_NAME, 'max-age=1234')
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
- it "allows you to specify includeSubdomains" do
198
- should_assign_header(HSTS_HEADER_NAME, "max-age=#{HSTS_MAX_AGE}; includeSubdomains")
199
- subject.set_hsts_header(:max_age => HSTS_MAX_AGE, :include_subdomains => true)
200
- end
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
- it "allows you to specify preload" do
203
- should_assign_header(HSTS_HEADER_NAME, "max-age=#{HSTS_MAX_AGE}; includeSubdomains; preload")
204
- subject.set_hsts_header(:max_age => HSTS_MAX_AGE, :include_subdomains => true, :preload => true)
205
- end
206
- end
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
- describe "#set_public_key_pins" do
209
- it "sets the Public-Key-Pins header" do
210
- should_assign_header(HPKP_HEADER_NAME + "-Report-Only", "max-age=1234")
211
- subject.set_hpkp_header(:max_age => 1234)
212
- end
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
- it "allows you to enforce public key pinning" do
215
- should_assign_header(HPKP_HEADER_NAME, "max-age=1234")
216
- subject.set_hpkp_header(:max_age => 1234, :enforce => true)
217
- end
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
- it "allows you to specific a custom max-age value" do
220
- should_assign_header(HPKP_HEADER_NAME + "-Report-Only", 'max-age=1234')
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
- it "allows you to specify includeSubdomains" do
225
- should_assign_header(HPKP_HEADER_NAME, "max-age=1234; includeSubDomains")
226
- subject.set_hpkp_header(:max_age => 1234, :include_subdomains => true, :enforce => true)
227
- end
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
- it "allows you to specify a report-uri" do
230
- should_assign_header(HPKP_HEADER_NAME, "max-age=1234; report-uri=\"https://foobar.com\"")
231
- subject.set_hpkp_header(:max_age => 1234, :report_uri => "https://foobar.com", :enforce => true)
232
- end
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
- it "allows you to specify a report-uri with app_name" do
235
- should_assign_header(HPKP_HEADER_NAME, "max-age=1234; report-uri=\"https://foobar.com?enforce=true&app_name=my_app\"")
236
- subject.set_hpkp_header(:max_age => 1234, :report_uri => "https://foobar.com", :app_name => "my_app", :tag_report_uri => true, :enforce => true)
237
- end
238
- end
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
- describe "#set_x_xss_protection" do
241
- it "sets the X-XSS-Protection header" do
242
- should_assign_header(X_XSS_PROTECTION_HEADER_NAME, SecureHeaders::XXssProtection::Constants::DEFAULT_VALUE)
243
- subject.set_x_xss_protection_header
244
- end
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
- it "sets a custom X-XSS-Protection header" do
247
- should_assign_header(X_XSS_PROTECTION_HEADER_NAME, '0')
248
- subject.set_x_xss_protection_header("0")
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
- it "sets the block flag" do
252
- should_assign_header(X_XSS_PROTECTION_HEADER_NAME, '1; mode=block')
253
- subject.set_x_xss_protection_header(:mode => 'block', :value => 1)
254
- end
255
- end
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
- describe "#set_x_content_type_options" do
258
- USER_AGENTS.each do |useragent|
259
- context "when using #{useragent}" do
260
- before(:each) do
261
- stub_user_agent(USER_AGENTS[useragent])
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
- it "sets the X-Content-Type-Options header" do
265
- should_assign_header(X_CONTENT_TYPE_OPTIONS_HEADER_NAME, SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE)
266
- subject.set_x_content_type_options_header
267
- end
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
- it "lets you override X-Content-Type-Options" do
270
- should_assign_header(X_CONTENT_TYPE_OPTIONS_HEADER_NAME, 'nosniff')
271
- subject.set_x_content_type_options_header(:value => 'nosniff')
272
- end
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
- describe "#set_csp_header" do
278
- context "when using Firefox" do
279
- it "sets CSP headers" do
280
- stub_user_agent(USER_AGENTS[:firefox])
281
- should_assign_header(HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER)
282
- subject.set_csp_header
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
- context "when using Chrome" do
287
- it "sets default CSP header" do
288
- stub_user_agent(USER_AGENTS[:chrome])
289
- should_assign_header(HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER)
290
- subject.set_csp_header
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
- context "when using a browser besides chrome/firefox" do
295
- it "sets the CSP header" do
296
- stub_user_agent(USER_AGENTS[:opera])
297
- should_assign_header(HEADER_NAME + "-Report-Only", DEFAULT_CSP_HEADER)
298
- subject.set_csp_header
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
- describe "#set_x_permitted_cross_domain_policies_header" do
304
- it "sets the X-Permitted-Cross-Domain-Policies header" do
305
- should_assign_header(XPCDP_HEADER_NAME, SecureHeaders::XPermittedCrossDomainPolicies::Constants::DEFAULT_VALUE)
306
- subject.set_x_permitted_cross_domain_policies_header
307
- end
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
- it "allows a custom X-Permitted-Cross-Domain-Policies header" do
310
- should_assign_header(XPCDP_HEADER_NAME, "master-only")
311
- subject.set_x_permitted_cross_domain_policies_header(:value => 'master-only')
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,37 +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
- if defined?(Coveralls)
7
- Coveralls.wear!
8
- end
9
-
10
- include ::SecureHeaders::PublicKeyPins::Constants
11
- include ::SecureHeaders::StrictTransportSecurity::Constants
12
- include ::SecureHeaders::ContentSecurityPolicy::Constants
13
- include ::SecureHeaders::XFrameOptions::Constants
14
- include ::SecureHeaders::XXssProtection::Constants
15
- include ::SecureHeaders::XContentTypeOptions::Constants
16
- include ::SecureHeaders::XDownloadOptions::Constants
17
- include ::SecureHeaders::XPermittedCrossDomainPolicies::Constants
8
+ ENV["RAILS_ENV"] = "test"
18
9
 
19
10
  USER_AGENTS = {
20
- :firefox => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1',
21
- :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',
22
- :ie => 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)',
23
- :opera => 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00',
24
- :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",
25
- :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",
26
- :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",
27
- :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",
28
- :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"
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"
29
20
  }
30
21
 
31
- def should_assign_header name, value
32
- expect(response.headers).to receive(:[]=).with(name, value)
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
33
40
  end
34
41
 
35
- def should_not_assign_header name
36
- expect(response.headers).not_to receive(:[]=).with(name, anything)
42
+ def reset_config
43
+ SecureHeaders::Configuration.clear_configurations
37
44
  end
@@ -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.