secure_headers 3.1.2 → 3.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3ebe74bc751469bb4305dd36b59fe4881a87ec3d
4
- data.tar.gz: 83fed6de7e4cab010cab6010caa0ba005ac43fc7
3
+ metadata.gz: a02f16867ec55f8c168ace664cc63d760785256f
4
+ data.tar.gz: db54c113af8919984c8f382b957834f38210cf3e
5
5
  SHA512:
6
- metadata.gz: bd02880ba737a5a9489dd6e8209420259fec8d78330f5992e7b34f61ebe16677a93ff123dd3b417f6973bfcd85e03524b053bcd1487458492aa4c7b8a7a9bb40
7
- data.tar.gz: 550c48cfd47e656e70dd3ab39ed6386b8d148231d55a553395da6076bc88bdaaa7fe1803695b093cf3e4fd49cb30bed0aff15a2455652efd00d7a27d9ce55636
6
+ metadata.gz: 6d9ddcb98a8c646d4e7a8cdb79bf3d8638cae4cb230ff268660e8eab7000fba99bb65e126ce55b3ff5f2a0346013fcf8b769dab4db697c82ab4461d444d0e2bd
7
+ data.tar.gz: b63c79ac3e9b37a4cff698f979f1ca6fd7e92567e35b1f9026be2e68a205e5badff302d512938a4d36517e8ec5b752d4833082e1b733ff73916fbf0b326641aa
data/CHANGELOG.md CHANGED
@@ -1,3 +1,120 @@
1
+ ## 3.2.0 Cookie settings and CSP hash sources
2
+
3
+ ### Cookies
4
+
5
+ SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.org/html/draft-west-first-party-cookies-07) cookies. These can be defined in the form of a boolean, or as a Hash for more refined configuration.
6
+
7
+ __Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests.
8
+
9
+ #### Boolean-based configuration
10
+
11
+ Boolean-based configuration is intended to globally enable or disable a specific cookie attribute.
12
+
13
+ ```ruby
14
+ config.cookies = {
15
+ secure: true, # mark all cookies as Secure
16
+ httponly: false, # do not mark any cookies as HttpOnly
17
+ }
18
+ ```
19
+
20
+ #### Hash-based configuration
21
+
22
+ Hash-based configuration allows for fine-grained control.
23
+
24
+ ```ruby
25
+ config.cookies = {
26
+ secure: { except: ['_guest'] }, # mark all but the `_guest` cookie as Secure
27
+ httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly
28
+ }
29
+ ```
30
+
31
+ #### SameSite cookie configuration
32
+
33
+ SameSite cookies permit either `Strict` or `Lax` enforcement mode options.
34
+
35
+ ```ruby
36
+ config.cookies = {
37
+ samesite: {
38
+ strict: true # mark all cookies as SameSite=Strict
39
+ }
40
+ }
41
+ ```
42
+
43
+ `Strict` and `Lax` enforcement modes can also be specified using a Hash.
44
+
45
+ ```ruby
46
+ config.cookies = {
47
+ samesite: {
48
+ strict: { only: ['_rails_session'] },
49
+ lax: { only: ['_guest'] }
50
+ }
51
+ }
52
+ ```
53
+
54
+ #### Hash
55
+
56
+ `script`/`style-src` hashes can be used to whitelist inline content that is static. This has the benefit of allowing inline content without opening up the possibility of dynamic javascript like you would with a `nonce`.
57
+
58
+ You can add hash sources directly to your policy :
59
+
60
+ ```ruby
61
+ ::SecureHeaders::Configuration.default do |config|
62
+ config.csp = {
63
+ default_src: %w('self')
64
+
65
+ # this is a made up value but browsers will show the expected hash in the console.
66
+ script_src: %w(sha256-123456)
67
+ }
68
+ end
69
+ ```
70
+
71
+ You can also use the automated inline script detection/collection/computation of hash source values in your app.
72
+
73
+ ```bash
74
+ rake secure_headers:generate_hashes
75
+ ```
76
+
77
+ This will generate a file (`config/config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header.
78
+
79
+ ```yaml
80
+ ---
81
+ scripts:
82
+ app/views/asdfs/index.html.erb:
83
+ - "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='"
84
+ styles:
85
+ app/views/asdfs/index.html.erb:
86
+ - "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='"
87
+ - "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='"
88
+ ```
89
+
90
+ ##### Helpers
91
+
92
+ **This will not compute dynamic hashes** by design. The output of both helpers will be a plain `script`/`style` tag without modification and the known hashes for a given file will be added to `script-src`/`style-src` when `hashed_javascript_tag` and `hashed_style_tag` are used. You can use `raise_error_on_unrecognized_hash = true` to be extra paranoid that you have precomputed hash values for all of your inline content. By default, this will raise an error in non-production environments.
93
+
94
+ ```erb
95
+ <%= hashed_style_tag do %>
96
+ body {
97
+ background-color: black;
98
+ }
99
+ <% end %>
100
+
101
+ <%= hashed_style_tag do %>
102
+ body {
103
+ font-size: 30px;
104
+ font-color: green;
105
+ }
106
+ <% end %>
107
+
108
+ <%= hashed_javascript_tag do %>
109
+ console.log(1)
110
+ <% end %>
111
+ ```
112
+
113
+ ```
114
+ Content-Security-Policy: ...
115
+ script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ;
116
+ style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...;
117
+
1
118
  ## 3.1.2 Bug fix for regression
2
119
 
3
120
  See https://github.com/twitter/secureheaders/pull/239
data/README.md CHANGED
@@ -15,7 +15,7 @@ The gem will automatically apply several headers that are related to security.
15
15
  - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html)
16
16
  - Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469)
17
17
 
18
- It can also mark all http cookies with the secure attribute (when configured to do so).
18
+ It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so).
19
19
 
20
20
  `secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings.
21
21
 
@@ -31,7 +31,13 @@ All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT`
31
31
 
32
32
  ```ruby
33
33
  SecureHeaders::Configuration.default do |config|
34
- config.secure_cookies = true # mark all cookies as "secure"
34
+ config.cookies = {
35
+ secure: true, # mark all cookies as "Secure"
36
+ httponly: true, # mark all cookies as "HttpOnly"
37
+ samesite: {
38
+ strict: true # mark all cookies as SameSite=Strict
39
+ }
40
+ }
35
41
  config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload"
36
42
  config.x_frame_options = "DENY"
37
43
  config.x_content_type_options = "nosniff"
@@ -195,24 +201,6 @@ Code | Result
195
201
 
196
202
  #### Nonce
197
203
 
198
- script/style-nonce can be used to whitelist inline content. To do this, call the `SecureHeaders.content_security_policy_nonce` then set the nonce attributes on the various tags.
199
-
200
- Setting a nonce will also set 'unsafe-inline' for browsers that don't support nonces for backwards compatibility. 'unsafe-inline' is ignored if a nonce is present in a directive in compliant browsers.
201
-
202
- ```erb
203
- <script nonce="<%= content_security_policy_nonce %>">
204
- console.log("whitelisted, will execute")
205
- </script>
206
-
207
- <script nonce="lol">
208
- console.log("won't execute, not whitelisted")
209
- </script>
210
-
211
- <script>
212
- console.log("won't execute, not whitelisted")
213
- </script>
214
- ```
215
-
216
204
  You can use a view helper to automatically add nonces to script tags:
217
205
 
218
206
  ```erb
@@ -240,9 +228,93 @@ body {
240
228
  </style>
241
229
  ```
242
230
 
231
+ ```
232
+
233
+ Content-Security-Policy: ...
234
+ script-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...;
235
+ style-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...;
236
+ ```
237
+
238
+ `script`/`style-nonce` can be used to whitelist inline content. To do this, call the `content_security_policy_script_nonce` or `content_security_policy_style_nonce` then set the nonce attributes on the various tags.
239
+
240
+ ```erb
241
+ <script nonce="<%= content_security_policy_script_nonce %>">
242
+ console.log("whitelisted, will execute")
243
+ </script>
244
+
245
+ <script nonce="lol">
246
+ console.log("won't execute, not whitelisted")
247
+ </script>
248
+
249
+ <script>
250
+ console.log("won't execute, not whitelisted")
251
+ </script>
252
+ ```
253
+
243
254
  #### Hash
244
255
 
245
- The hash feature has been removed, for now.
256
+ `script`/`style-src` hashes can be used to whitelist inline content that is static. This has the benefit of allowing inline content without opening up the possibility of dynamic javascript like you would with a `nonce`.
257
+
258
+ You can add hash sources directly to your policy :
259
+
260
+ ```ruby
261
+ ::SecureHeaders::Configuration.default do |config|
262
+ config.csp = {
263
+ default_src: %w('self')
264
+
265
+ # this is a made up value but browsers will show the expected hash in the console.
266
+ script_src: %w(sha256-123456)
267
+ }
268
+ end
269
+ ```
270
+
271
+ You can also use the automated inline script detection/collection/computation of hash source values in your app.
272
+
273
+ ```bash
274
+ rake secure_headers:generate_hashes
275
+ ```
276
+
277
+ This will generate a file (`config/config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header.
278
+
279
+ ```yaml
280
+ ---
281
+ scripts:
282
+ app/views/asdfs/index.html.erb:
283
+ - "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='"
284
+ styles:
285
+ app/views/asdfs/index.html.erb:
286
+ - "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='"
287
+ - "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='"
288
+ ```
289
+
290
+ ##### Helpers
291
+
292
+ **This will not compute dynamic hashes** by design. The output of both helpers will be a plain `script`/`style` tag without modification and the known hashes for a given file will be added to `script-src`/`style-src` when `hashed_javascript_tag` and `hashed_style_tag` are used. You can use `raise_error_on_unrecognized_hash = true` to be extra paranoid that you have precomputed hash values for all of your inline content. By default, this will raise an error in non-production environments.
293
+
294
+ ```erb
295
+ <%= hashed_style_tag do %>
296
+ body {
297
+ background-color: black;
298
+ }
299
+ <% end %>
300
+
301
+ <%= hashed_style_tag do %>
302
+ body {
303
+ font-size: 30px;
304
+ font-color: green;
305
+ }
306
+ <% end %>
307
+
308
+ <%= hashed_javascript_tag do %>
309
+ console.log(1)
310
+ <% end %>
311
+ ```
312
+
313
+ ```
314
+ Content-Security-Policy: ...
315
+ script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ;
316
+ style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...;
317
+ ```
246
318
 
247
319
  ### Public Key Pins
248
320
 
@@ -264,6 +336,57 @@ config.hpkp = {
264
336
  }
265
337
  ```
266
338
 
339
+ ### Cookies
340
+
341
+ SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.org/html/draft-west-first-party-cookies-07) cookies. These can be defined in the form of a boolean, or as a Hash for more refined configuration.
342
+
343
+ __Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests.
344
+
345
+ #### Boolean-based configuration
346
+
347
+ Boolean-based configuration is intended to globally enable or disable a specific cookie attribute.
348
+
349
+ ```ruby
350
+ config.cookies = {
351
+ secure: true, # mark all cookies as Secure
352
+ httponly: false, # do not mark any cookies as HttpOnly
353
+ }
354
+ ```
355
+
356
+ #### Hash-based configuration
357
+
358
+ Hash-based configuration allows for fine-grained control.
359
+
360
+ ```ruby
361
+ config.cookies = {
362
+ secure: { except: ['_guest'] }, # mark all but the `_guest` cookie as Secure
363
+ httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly
364
+ }
365
+ ```
366
+
367
+ #### SameSite cookie configuration
368
+
369
+ SameSite cookies permit either `Strict` or `Lax` enforcement mode options.
370
+
371
+ ```ruby
372
+ config.cookies = {
373
+ samesite: {
374
+ strict: true # mark all cookies as SameSite=Strict
375
+ }
376
+ }
377
+ ```
378
+
379
+ `Strict` and `Lax` enforcement modes can also be specified using a Hash.
380
+
381
+ ```ruby
382
+ config.cookies = {
383
+ samesite: {
384
+ strict: { only: ['_rails_session'] },
385
+ lax: { only: ['_guest'] }
386
+ }
387
+ }
388
+ ```
389
+
267
390
  ### Using with Sinatra
268
391
 
269
392
  Here's an example using SecureHeaders for Sinatra applications:
@@ -1,3 +1,5 @@
1
+ require 'yaml'
2
+
1
3
  module SecureHeaders
2
4
  class Configuration
3
5
  DEFAULT_CONFIG = :default
@@ -102,9 +104,16 @@ module SecureHeaders
102
104
 
103
105
  attr_writer :hsts, :x_frame_options, :x_content_type_options,
104
106
  :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
105
- :hpkp, :dynamic_csp, :secure_cookies
107
+ :hpkp, :dynamic_csp, :cookies
108
+
109
+ attr_reader :cached_headers, :csp, :dynamic_csp, :cookies
106
110
 
107
- attr_reader :cached_headers, :csp, :dynamic_csp, :secure_cookies
111
+ HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
112
+ if File.exists?(HASH_CONFIG_FILE)
113
+ config = YAML.safe_load(File.open(HASH_CONFIG_FILE))
114
+ @script_hashes = config["scripts"]
115
+ @style_hashes = config["styles"]
116
+ end
108
117
 
109
118
  def initialize(&block)
110
119
  self.hpkp = OPT_OUT
@@ -117,7 +126,7 @@ module SecureHeaders
117
126
  # Returns a deep-dup'd copy of this configuration.
118
127
  def dup
119
128
  copy = self.class.new
120
- copy.secure_cookies = @secure_cookies
129
+ copy.cookies = @cookies
121
130
  copy.csp = self.class.send(:deep_copy_if_hash, @csp)
122
131
  copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp)
123
132
  copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
@@ -172,13 +181,19 @@ module SecureHeaders
172
181
  XDownloadOptions.validate_config!(@x_download_options)
173
182
  XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
174
183
  PublicKeyPins.validate_config!(@hpkp)
184
+ Cookie.validate_config!(@cookies)
185
+ end
186
+
187
+ def secure_cookies=(secure_cookies)
188
+ Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#secure_cookies=` is deprecated. Please use `#cookies=` to configure secure cookies instead."
189
+ @cookies = (@cookies || {}).merge(secure: secure_cookies)
175
190
  end
176
191
 
177
192
  protected
178
193
 
179
194
  def csp=(new_csp)
180
195
  if self.dynamic_csp
181
- raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= isntead."
196
+ raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead."
182
197
  end
183
198
 
184
199
  @csp = new_csp
@@ -0,0 +1,10 @@
1
+ require 'base64'
2
+
3
+ module SecureHeaders
4
+ module HashHelper
5
+ def hash_source(inline_script, digest = :SHA256)
6
+ base64_hashed_content = Base64.encode64(Digest.const_get(digest).digest(inline_script)).chomp
7
+ "'#{digest.to_s.downcase}-#{base64_hashed_content}'"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,126 @@
1
+ require 'cgi'
2
+ require 'secure_headers/utils/cookies_config'
3
+
4
+ module SecureHeaders
5
+ class CookiesConfigError < StandardError; end
6
+ class Cookie
7
+
8
+ class << self
9
+ def validate_config!(config)
10
+ CookiesConfig.new(config).validate!
11
+ end
12
+ end
13
+
14
+ attr_reader :raw_cookie, :config
15
+
16
+ def initialize(cookie, config)
17
+ @raw_cookie = cookie
18
+ @config = config
19
+ @attributes = {
20
+ httponly: nil,
21
+ samesite: nil,
22
+ secure: nil,
23
+ }
24
+
25
+ parse(cookie)
26
+ end
27
+
28
+ def to_s
29
+ @raw_cookie.dup.tap do |c|
30
+ c << "; secure" if secure?
31
+ c << "; HttpOnly" if httponly?
32
+ c << "; #{samesite_cookie}" if samesite?
33
+ end
34
+ end
35
+
36
+ def secure?
37
+ flag_cookie?(:secure) && !already_flagged?(:secure)
38
+ end
39
+
40
+ def httponly?
41
+ flag_cookie?(:httponly) && !already_flagged?(:httponly)
42
+ end
43
+
44
+ def samesite?
45
+ flag_samesite? && !already_flagged?(:samesite)
46
+ end
47
+
48
+ private
49
+
50
+ def parsed_cookie
51
+ @parsed_cookie ||= CGI::Cookie.parse(raw_cookie)
52
+ end
53
+
54
+ def already_flagged?(attribute)
55
+ @attributes[attribute]
56
+ end
57
+
58
+ def flag_cookie?(attribute)
59
+ case config[attribute]
60
+ when TrueClass
61
+ true
62
+ when Hash
63
+ conditionally_flag?(config[attribute])
64
+ else
65
+ false
66
+ end
67
+ end
68
+
69
+ def conditionally_flag?(configuration)
70
+ if(Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?)
71
+ true
72
+ elsif(Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?)
73
+ true
74
+ else
75
+ false
76
+ end
77
+ end
78
+
79
+ def samesite_cookie
80
+ if flag_samesite_lax?
81
+ "SameSite=Lax"
82
+ elsif flag_samesite_strict?
83
+ "SameSite=Strict"
84
+ end
85
+ end
86
+
87
+ def flag_samesite?
88
+ flag_samesite_lax? || flag_samesite_strict?
89
+ end
90
+
91
+ def flag_samesite_lax?
92
+ flag_samesite_enforcement?(:lax)
93
+ end
94
+
95
+ def flag_samesite_strict?
96
+ flag_samesite_enforcement?(:strict)
97
+ end
98
+
99
+ def flag_samesite_enforcement?(mode)
100
+ return unless config[:samesite]
101
+
102
+ case config[:samesite][mode]
103
+ when Hash
104
+ conditionally_flag?(config[:samesite][mode])
105
+ when TrueClass
106
+ true
107
+ else
108
+ false
109
+ end
110
+ end
111
+
112
+ def parse(cookie)
113
+ return unless cookie
114
+
115
+ cookie.split(/[;,]\s?/).each do |pairs|
116
+ name, values = pairs.split('=',2)
117
+ name = CGI.unescape(name)
118
+
119
+ attribute = name.downcase.to_sym
120
+ if @attributes.has_key?(attribute)
121
+ @attributes[attribute] = values || true
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -1,7 +1,5 @@
1
1
  module SecureHeaders
2
2
  class Middleware
3
- SECURE_COOKIE_REGEXP = /;\s*secure\s*(;|$)/i.freeze
4
-
5
3
  def initialize(app)
6
4
  @app = app
7
5
  end
@@ -12,7 +10,7 @@ module SecureHeaders
12
10
  status, headers, response = @app.call(env)
13
11
 
14
12
  config = SecureHeaders.config_for(req)
15
- flag_cookies_as_secure!(headers) if config.secure_cookies
13
+ flag_cookies!(headers, override_secure(env, config.cookies)) if config.cookies
16
14
  headers.merge!(SecureHeaders.header_hash_for(req))
17
15
  [status, headers, response]
18
16
  end
@@ -20,19 +18,35 @@ module SecureHeaders
20
18
  private
21
19
 
22
20
  # inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194
23
- def flag_cookies_as_secure!(headers)
21
+ def flag_cookies!(headers, config)
24
22
  if cookies = headers['Set-Cookie']
25
23
  # Support Rails 2.3 / Rack 1.1 arrays as headers
26
24
  cookies = cookies.split("\n") unless cookies.is_a?(Array)
27
25
 
28
26
  headers['Set-Cookie'] = cookies.map do |cookie|
29
- if cookie !~ SECURE_COOKIE_REGEXP
30
- "#{cookie}; secure"
31
- else
32
- cookie
33
- end
27
+ SecureHeaders::Cookie.new(cookie, config).to_s
34
28
  end.join("\n")
35
29
  end
36
30
  end
31
+
32
+ # disable Secure cookies for non-https requests
33
+ def override_secure(env, config = {})
34
+ if scheme(env) != 'https'
35
+ config.merge!(secure: false)
36
+ end
37
+
38
+ config
39
+ end
40
+
41
+ # derived from https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L119
42
+ def scheme(env)
43
+ if env['HTTPS'] == 'on' || env['HTTP_X_SSL_REQUEST'] == 'on'
44
+ 'https'
45
+ elsif env['HTTP_X_FORWARDED_PROTO']
46
+ env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
47
+ else
48
+ env['rack.url_scheme']
49
+ end
50
+ end
37
51
  end
38
52
  end
@@ -13,6 +13,10 @@ if defined?(Rails::Railtie)
13
13
  Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware
14
14
  end
15
15
 
16
+ rake_tasks do
17
+ load File.expand_path(File.join('..', '..', 'lib', 'tasks', 'tasks.rake'), File.dirname(__FILE__))
18
+ end
19
+
16
20
  initializer "secure_headers.action_controller" do
17
21
  ActiveSupport.on_load(:action_controller) do
18
22
  include SecureHeaders
@@ -0,0 +1,94 @@
1
+ module SecureHeaders
2
+ class CookiesConfig
3
+
4
+ attr_reader :config
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def validate!
11
+ return if config.nil? || config == SecureHeaders::OPT_OUT
12
+
13
+ validate_config!
14
+ validate_secure_config! if config[:secure]
15
+ validate_httponly_config! if config[:httponly]
16
+ validate_samesite_config! if config[:samesite]
17
+ end
18
+
19
+ private
20
+
21
+ def validate_config!
22
+ raise CookiesConfigError.new("config must be a hash.") unless is_hash?(config)
23
+ end
24
+
25
+ def validate_secure_config!
26
+ validate_hash_or_boolean!(:secure)
27
+ validate_exclusive_use_of_hash_constraints!(config[:secure], :secure)
28
+ end
29
+
30
+ def validate_httponly_config!
31
+ validate_hash_or_boolean!(:httponly)
32
+ validate_exclusive_use_of_hash_constraints!(config[:httponly], :httponly)
33
+ end
34
+
35
+ def validate_samesite_config!
36
+ raise CookiesConfigError.new("samesite cookie config must be a hash") unless is_hash?(config[:samesite])
37
+
38
+ validate_samesite_boolean_config!
39
+ validate_samesite_hash_config!
40
+ end
41
+
42
+ # when configuring with booleans, only one enforcement is permitted
43
+ def validate_samesite_boolean_config!
44
+ if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict)
45
+ raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.")
46
+ elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax)
47
+ raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.")
48
+ end
49
+ end
50
+
51
+ def validate_samesite_hash_config!
52
+ # validate Hash-based samesite configuration
53
+ if is_hash?(config[:samesite][:lax])
54
+ validate_exclusive_use_of_hash_constraints!(config[:samesite][:lax], 'samesite lax')
55
+
56
+ if is_hash?(config[:samesite][:strict])
57
+ validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict], 'samesite strict')
58
+ validate_exclusive_use_of_samesite_enforcement!(:only)
59
+ validate_exclusive_use_of_samesite_enforcement!(:except)
60
+ end
61
+ end
62
+ end
63
+
64
+ def validate_hash_or_boolean!(attribute)
65
+ if !(is_hash?(config[attribute]) || is_boolean?(config[attribute]))
66
+ raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean")
67
+ end
68
+ end
69
+
70
+ # validate exclusive use of only or except but not both at the same time
71
+ def validate_exclusive_use_of_hash_constraints!(conf, attribute)
72
+ return unless is_hash?(conf)
73
+
74
+ if conf.key?(:only) && conf.key?(:except)
75
+ raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.")
76
+ end
77
+ end
78
+
79
+ # validate exclusivity of only and except members within strict and lax
80
+ def validate_exclusive_use_of_samesite_enforcement!(attribute)
81
+ if (intersection = (config[:samesite][:lax].fetch(attribute, []) & config[:samesite][:strict].fetch(attribute, []))).any?
82
+ raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict")
83
+ end
84
+ end
85
+
86
+ def is_hash?(obj)
87
+ obj && obj.is_a?(Hash)
88
+ end
89
+
90
+ def is_boolean?(obj)
91
+ obj && (obj.is_a?(TrueClass) || obj.is_a?(FalseClass))
92
+ end
93
+ end
94
+ end