secure_headers 3.9.0 → 4.0.1

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 (56) hide show
  1. checksums.yaml +5 -5
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +3 -0
  4. data/.ruby-version +1 -1
  5. data/.travis.yml +8 -6
  6. data/CHANGELOG.md +4 -16
  7. data/CONTRIBUTING.md +1 -1
  8. data/Gemfile +7 -4
  9. data/Guardfile +1 -0
  10. data/README.md +8 -40
  11. data/Rakefile +22 -18
  12. data/docs/cookies.md +18 -5
  13. data/docs/per_action_configuration.md +28 -0
  14. data/lib/secure_headers/configuration.rb +5 -12
  15. data/lib/secure_headers/hash_helper.rb +2 -1
  16. data/lib/secure_headers/headers/clear_site_data.rb +4 -3
  17. data/lib/secure_headers/headers/content_security_policy.rb +10 -19
  18. data/lib/secure_headers/headers/content_security_policy_config.rb +1 -0
  19. data/lib/secure_headers/headers/cookie.rb +22 -10
  20. data/lib/secure_headers/headers/expect_certificate_transparency.rb +2 -2
  21. data/lib/secure_headers/headers/policy_management.rb +14 -2
  22. data/lib/secure_headers/headers/public_key_pins.rb +4 -3
  23. data/lib/secure_headers/headers/referrer_policy.rb +1 -0
  24. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  25. data/lib/secure_headers/headers/x_content_type_options.rb +1 -0
  26. data/lib/secure_headers/headers/x_download_options.rb +2 -1
  27. data/lib/secure_headers/headers/x_frame_options.rb +1 -0
  28. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +2 -1
  29. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  30. data/lib/secure_headers/middleware.rb +10 -9
  31. data/lib/secure_headers/railtie.rb +7 -6
  32. data/lib/secure_headers/utils/cookies_config.rb +17 -18
  33. data/lib/secure_headers/view_helper.rb +2 -1
  34. data/lib/secure_headers.rb +2 -1
  35. data/lib/tasks/tasks.rake +2 -1
  36. data/secure_headers.gemspec +13 -3
  37. data/spec/lib/secure_headers/configuration_spec.rb +9 -8
  38. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +2 -1
  39. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +13 -27
  40. data/spec/lib/secure_headers/headers/cookie_spec.rb +58 -37
  41. data/spec/lib/secure_headers/headers/{expect_certificate_transparency_spec.rb → expect_certificate_spec.rb} +3 -3
  42. data/spec/lib/secure_headers/headers/policy_management_spec.rb +26 -10
  43. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
  44. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +4 -3
  45. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +5 -4
  46. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
  47. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
  48. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
  49. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
  50. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
  51. data/spec/lib/secure_headers/middleware_spec.rb +29 -21
  52. data/spec/lib/secure_headers/view_helpers_spec.rb +5 -4
  53. data/spec/lib/secure_headers_spec.rb +92 -120
  54. data/spec/spec_helper.rb +9 -22
  55. data/upgrading-to-4-0.md +55 -0
  56. metadata +16 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: 6e2f294d6c2130a1f629bf3dba2a88da71fa2b80a015cb7dc96bcbbcb88d1d59
4
- data.tar.gz: 767021dbec7d023bbcb629b3315ad10ff989e2c6ae7ceff9ffc5614b05fa3ba5
2
+ SHA1:
3
+ metadata.gz: fe8e39e4a81e0429bcba0fde48a5bb670f4bfb21
4
+ data.tar.gz: fc5989c5886abaf9245b2e29c3a0fbdb77e03e40
5
5
  SHA512:
6
- metadata.gz: 86d16e60409fca6b72e16321100a6afc3cc1fded1fb79c004778c8cdae2872ed4751205e90274f4b2c0517a70763c896692d9bd547f3b5b9144b8479b93268d5
7
- data.tar.gz: a2462e058ad88292d40e5a7e4e23e0a5ba431a8a114616f1e1d77bd5218e99ed0b68f4e7b0cf9e3480e10ca2e34e97d60e3b7b1d77db8b71da71c031f4cdaa52
6
+ metadata.gz: 373cbefd9bf90c0aa486de40aea8b14f45c9855a580048b25a1de9e7475cb597b7c4bda979f79c897fc988a9be38741477994ba295153ccb642846b767ab597d
7
+ data.tar.gz: 1911795207a61925df44c32d8ffb23908c909332eab7b579d84a5a008296be0e329fcc950e70885fd876b2c7feed2d4e2c12f65148303f90849757a050b32a04
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --order rand
2
2
  --warnings
3
+ --format progress
data/.rubocop.yml ADDED
@@ -0,0 +1,3 @@
1
+ inherit_gem:
2
+ rubocop-github:
3
+ - config/default.yml
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.3.3
1
+ 2.4.1
data/.travis.yml CHANGED
@@ -2,15 +2,17 @@ language: ruby
2
2
 
3
3
  rvm:
4
4
  - ruby-head
5
- - 2.4.0
6
- - 2.3.3
5
+ - 2.4.1
6
+ - 2.3.4
7
7
  - 2.2
8
- - 2.1
9
- - 2.0.0
10
- - 1.9.3
11
- - jruby-19mode
12
8
  - jruby-head
13
9
 
10
+ env:
11
+ - SUITE=rspec spec
12
+ - SUITE=rubocop
13
+
14
+ script: bundle exec $SUITE
15
+
14
16
  matrix:
15
17
  allow_failures:
16
18
  - rvm: jruby-head
data/CHANGELOG.md CHANGED
@@ -1,18 +1,10 @@
1
- ## 3.9.0
1
+ ## 4.0.1
2
2
 
3
- Fixes newline injection issue
3
+ - Adds support for `worker-src` CSP directive to 4.x line (https://github.com/twitter/secureheaders/pull/364)
4
4
 
5
- ## 3.8.0
5
+ ## 4.0
6
6
 
7
- Fixes semicolon injection issue reported by @mvgijssel see https://github.com/twitter/secure_headers/issues/418
8
-
9
- ## 3.7.4
10
-
11
- Backport SameSite=None functionality into 3.x line
12
-
13
- ## 3.7.3
14
-
15
- - Updates `Expect-CT` header to use a comma separator between directives, as specified in the most current spec.
7
+ - See the [upgrading to 4.0](upgrading-to-4-0.md) guide. Lots of breaking changes.
16
8
 
17
9
  ## 3.7.2
18
10
 
@@ -30,10 +22,6 @@ Adds support for the `Expect-CT` header (@jacobbednarz: https://github.com/twitt
30
22
 
31
23
  Actually set manifest-src when configured. https://github.com/twitter/secureheaders/pull/339 Thanks @carlosantoniodasilva!
32
24
 
33
- ## 3.6.6
34
-
35
- wat?
36
-
37
25
  ## 3.6.5
38
26
 
39
27
  Update clear-site-data header to use current format specified by the specification.
data/CONTRIBUTING.md CHANGED
@@ -30,7 +30,7 @@ Here are a few things you can do that will increase the likelihood of your pull
30
30
  0. Ensure CI is green
31
31
  0. Pull the latest code
32
32
  0. Increment the version
33
- 0. Run `gem build darrrr.gemspec`
33
+ 0. Run `gem build secure_headers.gemspec`
34
34
  0. Bump the Gemfile and Gemfile.lock versions for an app which relies on this gem
35
35
  0. Test behavior locally, branch deploy, whatever needs to happen
36
36
  0. Run `bundle exec rake release`
data/Gemfile CHANGED
@@ -1,19 +1,22 @@
1
+ # frozen_string_literal: true
1
2
  source "https://rubygems.org"
2
3
 
3
4
  gemspec
4
5
 
5
6
  group :test do
6
- gem "tins", "~> 1.6.0" # 1.7 requires ruby 2.0
7
- gem "pry-nav"
7
+ gem "coveralls"
8
8
  gem "json", "~> 1"
9
+ gem "pry-nav"
9
10
  gem "rack", "~> 1"
10
11
  gem "rspec"
11
- gem "coveralls"
12
+ gem "rubocop", "~> 0.47.0"
13
+ gem "rubocop-github"
12
14
  gem "term-ansicolor", "< 1.4"
15
+ gem "tins", "~> 1.6.0" # 1.7 requires ruby 2.0
13
16
  end
14
17
 
15
18
  group :guard do
16
- gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22]
17
19
  gem "growl"
20
+ gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22, :ruby_23, :ruby_24]
18
21
  gem "rb-fsevent"
19
22
  end
data/Guardfile CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  guard :rspec, cmd: "bundle exec rspec", all_on_start: true, all_after_pass: true do
2
3
  require "guard/rspec/dsl"
3
4
  dsl = Guard::RSpec::Dsl.new(self)
data/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.svg?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders)
2
2
 
3
+ **master represents the unreleased 4.x line**. See the [upgrading to 4.x doc](upgrading-to-4-0.md) for instructions on how to upgrade. Bug fixes should go in the 3.x branch for now.
3
4
 
4
- **The 3.x branch was recently merged**. See the [upgrading to 3.x doc](upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch.
5
+ **The [3.x](https://github.com/twitter/secureheaders/tree/2.x) branch is moving into maintenance mode**. See the [upgrading to 3.x doc](upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch.
5
6
 
6
- **The [2.x branch](https://github.com/twitter/secureheaders/tree/2.x) will be maintained**. The documentation below only applies to the 3.x branch. See the 2.x [README](https://github.com/twitter/secureheaders/blob/2.x/README.md) for the old way of doing things.
7
+ **The [2.x branch](https://github.com/twitter/secureheaders/tree/2.x) will be not be maintained once 4.x is released**. The documentation below only applies to the 3.x branch. See the 2.x [README](https://github.com/twitter/secureheaders/blob/2.x/README.md) for the old way of doing things.
7
8
 
8
9
  The gem will automatically apply several headers that are related to security. This includes:
9
10
  - Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/)
@@ -54,6 +55,8 @@ If you do not supply a `default` configuration, exceptions will be raised. If yo
54
55
 
55
56
  All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` will disable the header entirely.
56
57
 
58
+ **Word of caution:** The following is not a default configuration per se. It serves as a sample implementation of the configuration. You should read more about these headers and determine what is appropriate for your requirements.
59
+
57
60
  ```ruby
58
61
  SecureHeaders::Configuration.default do |config|
59
62
  config.cookies = {
@@ -64,27 +67,15 @@ SecureHeaders::Configuration.default do |config|
64
67
  }
65
68
  }
66
69
  # Add "; preload" and submit the site to hstspreload.org for best protection.
67
- config.hsts = "max-age=#{20.years.to_i}; includeSubdomains"
70
+ config.hsts = "max-age=#{20.years.to_i}"
68
71
  config.x_frame_options = "DENY"
69
72
  config.x_content_type_options = "nosniff"
70
73
  config.x_xss_protection = "1; mode=block"
71
74
  config.x_download_options = "noopen"
72
75
  config.x_permitted_cross_domain_policies = "none"
73
76
  config.referrer_policy = "origin-when-cross-origin"
74
- config.clear_site_data = [
75
- "cache",
76
- "cookies",
77
- "storage",
78
- "executionContexts"
79
- ]
80
- config.expect_certificate_transparency = {
81
- enforce: false,
82
- max_age: 1.day.to_i,
83
- report_uri: "https://report-uri.io/example-ct"
84
- }
85
77
  config.csp = {
86
- # "meta" values. these will shaped the header, but the values are not included in the header.
87
- # report_only: true, # default: false [DEPRECATED from 3.5.0: instead, configure csp_report_only]
78
+ # "meta" values. these will shape the header, but the values are not included in the header.
88
79
  preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
89
80
 
90
81
  # directive values: these values will directly translate into source directives
@@ -100,6 +91,7 @@ SecureHeaders::Configuration.default do |config|
100
91
  manifest_src: %w('self'),
101
92
  media_src: %w(utoob.com),
102
93
  object_src: %w('self'),
94
+ sandbox: true, # true and [] will set a maximally restrictive setting
103
95
  plugin_types: %w(application/x-shockwave-flash),
104
96
  script_src: %w('self'),
105
97
  style_src: %w('unsafe-inline'),
@@ -112,16 +104,6 @@ SecureHeaders::Configuration.default do |config|
112
104
  img_src: %w(somewhereelse.com),
113
105
  report_uri: %w(https://report-uri.io/example-csp-report-only)
114
106
  })
115
- config.hpkp = {
116
- report_only: false,
117
- max_age: 60.days.to_i,
118
- include_subdomains: true,
119
- report_uri: "https://report-uri.io/example-hpkp",
120
- pins: [
121
- {sha256: "abc"},
122
- {sha256: "123"}
123
- ]
124
- }
125
107
  end
126
108
  ```
127
109
 
@@ -139,20 +121,6 @@ X-Permitted-Cross-Domain-Policies: none
139
121
  X-Xss-Protection: 1; mode=block
140
122
  ```
141
123
 
142
- ### Default CSP
143
-
144
- By default, the above CSP will be applied to all requests. If you **only** want to set a Report-Only header, opt-out of the default enforced header for clarity. The configuration will assume that if you only supply `csp_report_only` that you intended to opt-out of `csp` but that's for the sake of backwards compatibility and it will be removed in the future.
145
-
146
- ```ruby
147
- Configuration.default do |config|
148
- config.csp = SecureHeaders::OPT_OUT # If this line is omitted, we will assume you meant to opt out.
149
- config.csp_report_only = {
150
- default_src: %w('self')
151
- }
152
- end
153
- ```
154
-
155
-
156
124
  ## Similar libraries
157
125
 
158
126
  * Rack [rack-secure_headers](https://github.com/frodsan/rack-secure_headers)
data/Rakefile CHANGED
@@ -1,28 +1,32 @@
1
1
  #!/usr/bin/env rake
2
- require 'bundler/gem_tasks'
3
- require 'rspec/core/rake_task'
4
- require 'net/http'
5
- require 'net/https'
2
+ # frozen_string_literal: true
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "net/http"
6
+ require "net/https"
6
7
 
7
- desc "Run RSpec"
8
- RSpec::Core::RakeTask.new do |t|
9
- t.verbose = false
10
- t.rspec_opts = "--format progress"
11
- end
12
-
13
- task default: :spec
8
+ RSpec::Core::RakeTask.new
14
9
 
15
10
  begin
16
- require 'rdoc/task'
11
+ require "rdoc/task"
17
12
  rescue LoadError
18
- require 'rdoc/rdoc'
19
- require 'rake/rdoctask'
13
+ require "rdoc/rdoc"
14
+ require "rake/rdoctask"
20
15
  RDoc::Task = Rake::RDocTask
21
16
  end
22
17
 
18
+ begin
19
+ require "rubocop/rake_task"
20
+ RuboCop::RakeTask.new
21
+ rescue LoadError
22
+ task(:rubocop) { $stderr.puts "RuboCop is disabled" }
23
+ end
24
+
23
25
  RDoc::Task.new(:rdoc) do |rdoc|
24
- rdoc.rdoc_dir = 'rdoc'
25
- rdoc.title = 'SecureHeaders'
26
- rdoc.options << '--line-numbers'
27
- rdoc.rdoc_files.include('lib/**/*.rb')
26
+ rdoc.rdoc_dir = "rdoc"
27
+ rdoc.title = "SecureHeaders"
28
+ rdoc.options << "--line-numbers"
29
+ rdoc.rdoc_files.include("lib/**/*.rb")
28
30
  end
31
+
32
+ task default: [:spec, :rubocop]
data/docs/cookies.md CHANGED
@@ -4,14 +4,28 @@ SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.
4
4
 
5
5
  __Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests.
6
6
 
7
+ #### Defaults
8
+
9
+ By default, all cookies will get both `Secure`, `HttpOnly`, and `SameSite=Lax`.
10
+
11
+ ```ruby
12
+ config.cookies = {
13
+ secure: true, # defaults to true but will be a no op on non-HTTPS requests
14
+ httponly: true, # defaults to true
15
+ samesite: { # defaults to set `SameSite=Lax`
16
+ lax: true
17
+ }
18
+ }
19
+ ```
20
+
7
21
  #### Boolean-based configuration
8
22
 
9
- Boolean-based configuration is intended to globally enable or disable a specific cookie attribute.
23
+ Boolean-based configuration is intended to globally enable or disable a specific cookie attribute. *Note: As of 4.0, you must use OPT_OUT rather than false to opt out of the defaults.*
10
24
 
11
25
  ```ruby
12
26
  config.cookies = {
13
27
  secure: true, # mark all cookies as Secure
14
- httponly: false, # do not mark any cookies as HttpOnly
28
+ httponly: OPT_OUT, # do not mark any cookies as HttpOnly
15
29
  }
16
30
  ```
17
31
 
@@ -38,14 +52,13 @@ config.cookies = {
38
52
  }
39
53
  ```
40
54
 
41
- `Strict`, `Lax`, and `None` enforcement modes can also be specified using a Hash.
55
+ `Strict` and `Lax` enforcement modes can also be specified using a Hash.
42
56
 
43
57
  ```ruby
44
58
  config.cookies = {
45
59
  samesite: {
46
60
  strict: { only: ['_rails_session'] },
47
- lax: { only: ['_guest'] },
48
- none: { only: ['_tracking'] },
61
+ lax: { only: ['_guest'] }
49
62
  }
50
63
  }
51
64
  ```
@@ -103,3 +103,31 @@ Content-Security-Policy: ...
103
103
  console.log("won't execute, not whitelisted")
104
104
  </script>
105
105
  ```
106
+
107
+ ## Clearing browser cache
108
+
109
+ You can clear the browser cache after the logout request by using the following.
110
+
111
+ ``` ruby
112
+ class ApplicationController < ActionController::Base
113
+ # Configuration override to send the Clear-Site-Data header.
114
+ SecureHeaders::Configuration.override(:clear_browser_cache) do |config|
115
+ config.clear_site_data = [
116
+ SecureHeaders::ClearSiteData::ALL_TYPES
117
+ ]
118
+ end
119
+
120
+
121
+ # Clears the browser's cache for browsers supporting the Clear-Site-Data
122
+ # header.
123
+ #
124
+ # Returns nothing.
125
+ def clear_browser_cache
126
+ SecureHeaders.use_secure_headers_override(request, :clear_browser_cache)
127
+ end
128
+ end
129
+
130
+ class SessionsController < ApplicationController
131
+ after_action :clear_browser_cache, only: :destroy
132
+ end
133
+ ```
@@ -1,4 +1,5 @@
1
- require 'yaml'
1
+ # frozen_string_literal: true
2
+ require "yaml"
2
3
 
3
4
  module SecureHeaders
4
5
  class Configuration
@@ -208,8 +209,7 @@ module SecureHeaders
208
209
  end
209
210
 
210
211
  def secure_cookies=(secure_cookies)
211
- Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#secure_cookies=` is deprecated. Please use `#cookies=` to configure secure cookies instead."
212
- @cookies = (@cookies || {}).merge(secure: secure_cookies)
212
+ raise ArgumentError, "#{Kernel.caller.first}: `#secure_cookies=` is no longer supported. Please use `#cookies=` to configure secure cookies instead."
213
213
  end
214
214
 
215
215
  def csp=(new_csp)
@@ -217,10 +217,8 @@ module SecureHeaders
217
217
  @csp = new_csp.dup
218
218
  else
219
219
  if new_csp[:report_only]
220
- # Deprecated configuration implies that CSPRO should be set, CSP should not - so opt out
221
- Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp=` was supplied a config with report_only: true. Use #csp_report_only="
222
- @csp = OPT_OUT
223
- self.csp_report_only = new_csp
220
+ # invalid configuration implies that CSPRO should be set, CSP should not - so opt out
221
+ raise ArgumentError, "#{Kernel.caller.first}: `#csp=` was supplied a config with report_only: true. Use #csp_report_only="
224
222
  else
225
223
  @csp = ContentSecurityPolicyConfig.new(new_csp)
226
224
  end
@@ -247,11 +245,6 @@ module SecureHeaders
247
245
  end
248
246
  end
249
247
  end
250
-
251
- if !@csp_report_only.opt_out? && @csp.to_h == ContentSecurityPolicyConfig::DEFAULT
252
- Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp_report_only=` was configured before `#csp=`. It is assumed you intended to opt out of `#csp=` so be sure to add `config.csp = SecureHeaders::OPT_OUT` to your config. Ensure that #csp_report_only is configured after #csp="
253
- @csp = OPT_OUT
254
- end
255
248
  end
256
249
 
257
250
  protected
@@ -1,4 +1,5 @@
1
- require 'base64'
1
+ # frozen_string_literal: true
2
+ require "base64"
2
3
 
3
4
  module SecureHeaders
4
5
  module HashHelper
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class ClearSiteDataConfigError < StandardError; end
3
4
  class ClearSiteData
@@ -7,8 +8,8 @@ module SecureHeaders
7
8
  CACHE = "cache".freeze
8
9
  COOKIES = "cookies".freeze
9
10
  STORAGE = "storage".freeze
10
- EXECTION_CONTEXTS = "executionContexts".freeze
11
- ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECTION_CONTEXTS]
11
+ EXECUTION_CONTEXTS = "executionContexts".freeze
12
+ ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECUTION_CONTEXTS]
12
13
 
13
14
  CONFIG_KEY = :clear_site_data
14
15
 
@@ -16,7 +17,7 @@ module SecureHeaders
16
17
  # Public: make an Clear-Site-Data header name, value pair
17
18
  #
18
19
  # Returns nil if not configured, returns header name and value if configured.
19
- def make_header(config=nil)
20
+ def make_header(config = nil)
20
21
  case config
21
22
  when nil, OPT_OUT, []
22
23
  # noop
@@ -1,6 +1,7 @@
1
- require_relative 'policy_management'
2
- require_relative 'content_security_policy_config'
3
- require 'useragent'
1
+ # frozen_string_literal: true
2
+ require_relative "policy_management"
3
+ require_relative "content_security_policy_config"
4
+ require "useragent"
4
5
 
5
6
  module SecureHeaders
6
7
  class ContentSecurityPolicy
@@ -54,18 +55,12 @@ module SecureHeaders
54
55
 
55
56
  private
56
57
 
57
- # frame-src is deprecated, child-src is being implemented. They are
58
- # very similar and in most cases, the same value can be used for both.
59
58
  def normalize_child_frame_src
60
59
  if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src
61
- Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers.")
60
+ raise ArgumentError, "#{Kernel.caller.first}: both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers."
62
61
  end
63
62
 
64
- if supported_directives.include?(:child_src)
65
- @config.child_src || @config.frame_src
66
- else
67
- @config.frame_src || @config.child_src
68
- end
63
+ @config.frame_src || @config.child_src
69
64
  end
70
65
 
71
66
  # Private: converts the config object into a string representing a policy.
@@ -141,15 +136,11 @@ module SecureHeaders
141
136
  else
142
137
  @config.directive_value(directive)
143
138
  end
144
- return unless source_list && source_list.any?
145
- normalized_source_list = minify_source_list(directive, source_list).join(" ")
146
139
 
147
- if normalized_source_list =~ /(\n|;)/
148
- Kernel.warn("#{directive} contains a #{$1} in #{normalized_source_list.inspect} which will raise an error in future versions. It has been replaced with a blank space.")
140
+ if source_list != OPT_OUT && source_list && source_list.any?
141
+ normalized_source_list = minify_source_list(directive, source_list)
142
+ [symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
149
143
  end
150
- escaped_source_list = normalized_source_list.gsub(/[\n;]/, " ")
151
-
152
- [symbol_to_hyphen_case(directive), escaped_source_list].join(" ").strip
153
144
  end
154
145
 
155
146
  # If a directive contains *, all other values are omitted.
@@ -270,7 +261,7 @@ module SecureHeaders
270
261
  end
271
262
 
272
263
  def symbol_to_hyphen_case(sym)
273
- sym.to_s.tr('_', '-')
264
+ sym.to_s.tr("_", "-")
274
265
  end
275
266
  end
276
267
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  module DynamicConfig
3
4
  def self.included(base)
@@ -1,5 +1,7 @@
1
- require 'cgi'
2
- require 'secure_headers/utils/cookies_config'
1
+ # frozen_string_literal: true
2
+ require "cgi"
3
+ require "secure_headers/utils/cookies_config"
4
+
3
5
 
4
6
  module SecureHeaders
5
7
  class CookiesConfigError < StandardError; end
@@ -13,8 +15,18 @@ module SecureHeaders
13
15
 
14
16
  attr_reader :raw_cookie, :config
15
17
 
18
+ COOKIE_DEFAULTS = {
19
+ httponly: true,
20
+ secure: true,
21
+ samesite: { lax: true },
22
+ }.freeze
23
+
16
24
  def initialize(cookie, config)
17
25
  @raw_cookie = cookie
26
+ unless config == OPT_OUT
27
+ config ||= {}
28
+ config = COOKIE_DEFAULTS.merge(config)
29
+ end
18
30
  @config = config
19
31
  @attributes = {
20
32
  httponly: nil,
@@ -56,6 +68,7 @@ module SecureHeaders
56
68
  end
57
69
 
58
70
  def flag_cookie?(attribute)
71
+ return false if config == OPT_OUT
59
72
  case config[attribute]
60
73
  when TrueClass
61
74
  true
@@ -81,13 +94,12 @@ module SecureHeaders
81
94
  "SameSite=Lax"
82
95
  elsif flag_samesite_strict?
83
96
  "SameSite=Strict"
84
- elsif flag_samesite_none?
85
- "SameSite=None"
86
97
  end
87
98
  end
88
99
 
89
100
  def flag_samesite?
90
- flag_samesite_lax? || flag_samesite_strict? || flag_samesite_none?
101
+ return false if config == OPT_OUT || config[:samesite] == OPT_OUT
102
+ flag_samesite_lax? || flag_samesite_strict?
91
103
  end
92
104
 
93
105
  def flag_samesite_lax?
@@ -98,13 +110,13 @@ module SecureHeaders
98
110
  flag_samesite_enforcement?(:strict)
99
111
  end
100
112
 
101
- def flag_samesite_none?
102
- flag_samesite_enforcement?(:none)
103
- end
104
-
105
113
  def flag_samesite_enforcement?(mode)
106
114
  return unless config[:samesite]
107
115
 
116
+ if config[:samesite].is_a?(TrueClass) && mode == :lax
117
+ return true
118
+ end
119
+
108
120
  case config[:samesite][mode]
109
121
  when Hash
110
122
  conditionally_flag?(config[:samesite][mode])
@@ -119,7 +131,7 @@ module SecureHeaders
119
131
  return unless cookie
120
132
 
121
133
  cookie.split(/[;,]\s?/).each do |pairs|
122
- name, values = pairs.split('=',2)
134
+ name, values = pairs.split("=", 2)
123
135
  name = CGI.unescape(name)
124
136
 
125
137
  attribute = name.downcase.to_sym
@@ -45,11 +45,11 @@ module SecureHeaders
45
45
  end
46
46
 
47
47
  def value
48
- header_value = [
48
+ [
49
49
  enforced_directive,
50
50
  max_age_directive,
51
51
  report_uri_directive
52
- ].compact.join(", ").strip
52
+ ].compact.join("; ").strip
53
53
  end
54
54
 
55
55
  def enforced_directive
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  module PolicyManagement
3
4
  def self.included(base)
@@ -5,8 +6,14 @@ module SecureHeaders
5
6
  end
6
7
 
7
8
  MODERN_BROWSERS = %w(Chrome Opera Firefox)
8
- DEFAULT_VALUE = "default-src https:".freeze
9
- DEFAULT_CONFIG = { default_src: %w(https:) }.freeze
9
+ DEFAULT_CONFIG = {
10
+ default_src: %w(https:),
11
+ img_src: %w(https: data: 'self'),
12
+ object_src: %w('none'),
13
+ script_src: %w(https:),
14
+ style_src: %w('self' 'unsafe-inline' https:),
15
+ form_action: %w('self')
16
+ }.freeze
10
17
  DATA_PROTOCOL = "data:".freeze
11
18
  BLOB_PROTOCOL = "blob:".freeze
12
19
  SELF = "'self'".freeze
@@ -205,6 +212,10 @@ module SecureHeaders
205
212
  def validate_config!(config)
206
213
  return if config.nil? || config.opt_out?
207
214
  raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src)
215
+ if config.directive_value(:script_src).nil?
216
+ raise ContentSecurityPolicyConfigError.new(":script_src is required, falling back to default-src is too dangerous. Use `script_src: OPT_OUT` to override")
217
+ end
218
+
208
219
  ContentSecurityPolicyConfig.attrs.each do |key|
209
220
  value = config.directive_value(key)
210
221
  next unless value
@@ -389,6 +400,7 @@ module SecureHeaders
389
400
  end
390
401
 
391
402
  def ensure_valid_sources!(directive, source_expression)
403
+ return if source_expression == OPT_OUT
392
404
  source_expression.each do |expression|
393
405
  if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(expression)
394
406
  raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{expression}). This value must be single quoted.")
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class PublicKeyPinsConfigError < StandardError; end
3
4
  class PublicKeyPins
@@ -54,7 +55,7 @@ module SecureHeaders
54
55
  pin_directives,
55
56
  report_uri_directive,
56
57
  subdomain_directive
57
- ].compact.join('; ').strip
58
+ ].compact.join("; ").strip
58
59
  end
59
60
 
60
61
  def pin_directives
@@ -63,7 +64,7 @@ module SecureHeaders
63
64
  pin.map do |token, hash|
64
65
  "pin-#{token}=\"#{hash}\"" if HASH_ALGORITHMS.include?(token)
65
66
  end
66
- end.join('; ')
67
+ end.join("; ")
67
68
  end
68
69
 
69
70
  def max_age_directive
@@ -75,7 +76,7 @@ module SecureHeaders
75
76
  end
76
77
 
77
78
  def subdomain_directive
78
- @include_subdomains ? 'includeSubDomains' : nil
79
+ @include_subdomains ? "includeSubDomains" : nil
79
80
  end
80
81
  end
81
82
  end