secure_headers 3.2.0 → 4.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +41 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +3 -0
  6. data/.ruby-version +1 -1
  7. data/.travis.yml +8 -4
  8. data/CHANGELOG.md +144 -1
  9. data/CODE_OF_CONDUCT.md +46 -0
  10. data/CONTRIBUTING.md +41 -0
  11. data/Gemfile +9 -4
  12. data/Guardfile +1 -0
  13. data/LICENSE +4 -199
  14. data/README.md +73 -344
  15. data/Rakefile +22 -18
  16. data/docs/HPKP.md +17 -0
  17. data/docs/cookies.md +64 -0
  18. data/docs/hashes.md +64 -0
  19. data/docs/named_overrides_and_appends.md +107 -0
  20. data/docs/per_action_configuration.md +133 -0
  21. data/docs/sinatra.md +25 -0
  22. data/lib/secure_headers/configuration.rb +116 -42
  23. data/lib/secure_headers/hash_helper.rb +2 -1
  24. data/lib/secure_headers/headers/clear_site_data.rb +55 -0
  25. data/lib/secure_headers/headers/content_security_policy.rb +128 -39
  26. data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
  27. data/lib/secure_headers/headers/cookie.rb +21 -3
  28. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  29. data/lib/secure_headers/headers/policy_management.rb +158 -68
  30. data/lib/secure_headers/headers/public_key_pins.rb +5 -4
  31. data/lib/secure_headers/headers/referrer_policy.rb +37 -0
  32. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  33. data/lib/secure_headers/headers/x_content_type_options.rb +2 -1
  34. data/lib/secure_headers/headers/x_download_options.rb +3 -2
  35. data/lib/secure_headers/headers/x_frame_options.rb +2 -1
  36. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
  37. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  38. data/lib/secure_headers/middleware.rb +16 -9
  39. data/lib/secure_headers/railtie.rb +7 -6
  40. data/lib/secure_headers/utils/cookies_config.rb +13 -12
  41. data/lib/secure_headers/view_helper.rb +15 -8
  42. data/lib/secure_headers.rb +139 -55
  43. data/lib/tasks/tasks.rake +7 -5
  44. data/secure_headers.gemspec +13 -3
  45. data/spec/lib/secure_headers/configuration_spec.rb +13 -12
  46. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
  47. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +89 -15
  48. data/spec/lib/secure_headers/headers/cookie_spec.rb +38 -20
  49. data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
  50. data/spec/lib/secure_headers/headers/policy_management_spec.rb +80 -42
  51. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
  52. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +73 -0
  53. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +5 -4
  54. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
  55. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
  56. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
  57. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
  58. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
  59. data/spec/lib/secure_headers/middleware_spec.rb +55 -17
  60. data/spec/lib/secure_headers/view_helpers_spec.rb +21 -8
  61. data/spec/lib/secure_headers_spec.rb +330 -58
  62. data/spec/spec_helper.rb +17 -25
  63. data/upgrading-to-3-0.md +13 -10
  64. data/upgrading-to-4-0.md +55 -0
  65. metadata +34 -7
data/docs/hashes.md ADDED
@@ -0,0 +1,64 @@
1
+ ## Hash
2
+
3
+ `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`.
4
+
5
+ You can add hash sources directly to your policy :
6
+
7
+ ```ruby
8
+ ::SecureHeaders::Configuration.default do |config|
9
+ config.csp = {
10
+ default_src: %w('self')
11
+
12
+ # this is a made up value but browsers will show the expected hash in the console.
13
+ script_src: %w(sha256-123456)
14
+ }
15
+ end
16
+ ```
17
+
18
+ You can also use the automated inline script detection/collection/computation of hash source values in your app.
19
+
20
+ ```bash
21
+ rake secure_headers:generate_hashes
22
+ ```
23
+
24
+ This will generate a file (`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.
25
+
26
+ ```yaml
27
+ ---
28
+ scripts:
29
+ app/views/asdfs/index.html.erb:
30
+ - "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='"
31
+ styles:
32
+ app/views/asdfs/index.html.erb:
33
+ - "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='"
34
+ - "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='"
35
+ ```
36
+
37
+ ##### Helpers
38
+
39
+ **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.
40
+
41
+ ```erb
42
+ <%= hashed_style_tag do %>
43
+ body {
44
+ background-color: black;
45
+ }
46
+ <% end %>
47
+
48
+ <%= hashed_style_tag do %>
49
+ body {
50
+ font-size: 30px;
51
+ font-color: green;
52
+ }
53
+ <% end %>
54
+
55
+ <%= hashed_javascript_tag do %>
56
+ console.log(1)
57
+ <% end %>
58
+ ```
59
+
60
+ ```
61
+ Content-Security-Policy: ...
62
+ script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ;
63
+ style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...;
64
+ ```
@@ -0,0 +1,107 @@
1
+ ## Named Appends
2
+
3
+ Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility.
4
+
5
+ ```ruby
6
+ def show
7
+ if include_widget?
8
+ @widget = widget.render
9
+ use_content_security_policy_named_append(:widget_partial)
10
+ end
11
+ end
12
+
13
+
14
+ SecureHeaders::Configuration.named_append(:widget_partial) do |request|
15
+ SecureHeaders.override_x_frame_options(request, "DENY")
16
+ if request.controller_instance.current_user.in_test_bucket?
17
+ { child_src: %w(beta.thirdpartyhost.com) }
18
+ else
19
+ { child_src: %w(thirdpartyhost.com) }
20
+ end
21
+ end
22
+ ```
23
+
24
+ You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following:
25
+
26
+ ```ruby
27
+ SecureHeader::Configuration.default do |config|
28
+ config.csp = { default_src: %w('self')}
29
+ end
30
+
31
+ SecureHeaders::Configuration.named_append(:A) do |request|
32
+ { default_src: %w(myhost.com) }
33
+ end
34
+
35
+ SecureHeaders::Configuration.named_append(:B) do |request|
36
+ { script_src: %w('unsafe-eval') }
37
+ end
38
+ ```
39
+
40
+ The following code will produce different policies due to the way policies are normalized (e.g. providing a previously undefined directive that inherits from `default-src`, removing host source values when `*` is provided. Removing `'none'` when additional values are present, etc.):
41
+
42
+ ```ruby
43
+ def index
44
+ use_content_security_policy_named_append(:A)
45
+ use_content_security_policy_named_append(:B)
46
+ # produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval';
47
+ end
48
+
49
+ def show
50
+ use_content_security_policy_named_append(:B)
51
+ use_content_security_policy_named_append(:A)
52
+ # produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval';
53
+ end
54
+ ```
55
+
56
+
57
+ ## Named overrides
58
+
59
+ Named overrides serve two purposes:
60
+
61
+ * To be able to refer to a configuration by simple name.
62
+ * By precomputing the headers for a named configuration, the headers generated once and reused over every request.
63
+
64
+ To use a named override, drop a `SecureHeaders::Configuration.override` block **outside** of method definitions and then declare which named override you'd like to use. You can even override an override.
65
+
66
+ ```ruby
67
+ class ApplicationController < ActionController::Base
68
+ SecureHeaders::Configuration.default do |config|
69
+ config.csp = {
70
+ default_src: %w('self'),
71
+ script_src: %w(example.org)
72
+ }
73
+ end
74
+
75
+ # override default configuration
76
+ SecureHeaders::Configuration.override(:script_from_otherdomain_com) do |config|
77
+ config.csp[:script_src] << "otherdomain.com"
78
+ end
79
+
80
+ # overrides the :script_from_otherdomain_com configuration
81
+ SecureHeaders::Configuration.override(:another_config, :script_from_otherdomain_com) do |config|
82
+ config.csp[:script_src] << "evenanotherdomain.com"
83
+ end
84
+ end
85
+
86
+ class MyController < ApplicationController
87
+ def index
88
+ # Produces default-src 'self'; script-src example.org otherdomain.com
89
+ use_secure_headers_override(:script_from_otherdomain_com)
90
+ end
91
+
92
+ def show
93
+ # Produces default-src 'self'; script-src example.org otherdomain.org evenanotherdomain.com
94
+ use_secure_headers_override(:another_config)
95
+ end
96
+ end
97
+ ```
98
+
99
+ By default, a no-op configuration is provided. No headers will be set when this default override is used.
100
+
101
+ ```ruby
102
+ class MyController < ApplicationController
103
+ def index
104
+ SecureHeaders.opt_out_of_all_protection(request)
105
+ end
106
+ end
107
+ ```
@@ -0,0 +1,133 @@
1
+ ## Per-action configuration
2
+
3
+ You can override the settings for a given action by producing a temporary override. Be aware that because of the dynamic nature of the value, the header values will be computed per request.
4
+
5
+ ```ruby
6
+ # Given a config of:
7
+ ::SecureHeaders::Configuration.default do |config|
8
+ config.csp = {
9
+ default_src: %w('self'),
10
+ script_src: %w('self')
11
+ }
12
+ end
13
+
14
+ class MyController < ApplicationController
15
+ def index
16
+ # Append value to the source list, override 'none' values
17
+ # Produces: default-src 'self'; script-src 'self' s3.amazonaws.com; object-src 'self' www.youtube.com
18
+ append_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self' www.youtube.com))
19
+
20
+ # Overrides the previously set source list, override 'none' values
21
+ # Produces: default-src 'self'; script-src s3.amazonaws.com; object-src 'self'
22
+ override_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self'))
23
+
24
+ # Global settings default to "sameorigin"
25
+ override_x_frame_options("DENY")
26
+ end
27
+ ```
28
+
29
+ The following methods are available as controller instance methods. They are also available as class methods, but require you to pass in the `request` object.
30
+ * `append_content_security_policy_directives(hash)`: appends each value to the corresponding CSP app-wide configuration.
31
+ * `override_content_security_policy_directives(hash)`: merges the hash into the app-wide configuration, overwriting any previous config
32
+ * `override_x_frame_options(value)`: sets the `X-Frame-Options header` to `value`
33
+
34
+ ## Appending / overriding Content Security Policy
35
+
36
+ When manipulating content security policy, there are a few things to consider. The default header value is `default-src https:` which corresponds to a default configuration of `{ default_src: %w(https:)}`.
37
+
38
+ #### Append to the policy with a directive other than `default_src`
39
+
40
+ The value of `default_src` is joined with the addition if the it is a [fetch directive](https://w3c.github.io/webappsec-csp/#directives-fetch). Note the `https:` is carried over from the `default-src` config. If you do not want this, use `override_content_security_policy_directives` instead. To illustrate:
41
+
42
+ ```ruby
43
+ ::SecureHeaders::Configuration.default do |config|
44
+ config.csp = {
45
+ default_src: %w('self')
46
+ }
47
+ end
48
+ ```
49
+
50
+ Code | Result
51
+ ------------- | -------------
52
+ `append_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src 'self' mycdn.com`
53
+ `override_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src mycdn.com`
54
+
55
+ #### Nonce
56
+
57
+ You can use a view helper to automatically add nonces to script tags:
58
+
59
+ ```erb
60
+ <%= nonced_javascript_tag do %>
61
+ console.log("nonced!");
62
+ <% end %>
63
+
64
+ <%= nonced_style_tag do %>
65
+ body {
66
+ background-color: black;
67
+ }
68
+ <% end %>
69
+ ```
70
+
71
+ becomes:
72
+
73
+ ```html
74
+ <script nonce="/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=">
75
+ console.log("nonced!")
76
+ </script>
77
+ <style nonce="/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=">
78
+ body {
79
+ background-color: black;
80
+ }
81
+ </style>
82
+ ```
83
+
84
+ ```
85
+
86
+ Content-Security-Policy: ...
87
+ script-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...;
88
+ style-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...;
89
+ ```
90
+
91
+ `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.
92
+
93
+ ```erb
94
+ <script nonce="<%= content_security_policy_script_nonce %>">
95
+ console.log("whitelisted, will execute")
96
+ </script>
97
+
98
+ <script nonce="lol">
99
+ console.log("won't execute, not whitelisted")
100
+ </script>
101
+
102
+ <script>
103
+ console.log("won't execute, not whitelisted")
104
+ </script>
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
+ ```
data/docs/sinatra.md ADDED
@@ -0,0 +1,25 @@
1
+ ## Sinatra
2
+
3
+ Here's an example using SecureHeaders for Sinatra applications:
4
+
5
+ ```ruby
6
+ require 'rubygems'
7
+ require 'sinatra'
8
+ require 'haml'
9
+ require 'secure_headers'
10
+
11
+ use SecureHeaders::Middleware
12
+
13
+ SecureHeaders::Configuration.default do |config|
14
+ ...
15
+ end
16
+
17
+ class Donkey < Sinatra::Application
18
+ set :root, APP_ROOT
19
+
20
+ get '/' do
21
+ SecureHeaders.override_x_frame_options(request, SecureHeaders::OPT_OUT)
22
+ haml :index
23
+ end
24
+ end
25
+ ```
@@ -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
@@ -31,7 +32,7 @@ module SecureHeaders
31
32
  raise NotYetConfiguredError, "#{base} policy not yet supplied"
32
33
  end
33
34
  override = @configurations[base].dup
34
- override.instance_eval &block if block_given?
35
+ override.instance_eval(&block) if block_given?
35
36
  add_configuration(name, override)
36
37
  end
37
38
 
@@ -46,6 +47,17 @@ module SecureHeaders
46
47
  @configurations[name]
47
48
  end
48
49
 
50
+ def named_appends(name)
51
+ @appends ||= {}
52
+ @appends[name]
53
+ end
54
+
55
+ def named_append(name, target = nil, &block)
56
+ @appends ||= {}
57
+ raise "Provide a configuration block" unless block_given?
58
+ @appends[name] = block
59
+ end
60
+
49
61
  private
50
62
 
51
63
  # Private: add a valid configuration to the global set of named configs.
@@ -61,6 +73,7 @@ module SecureHeaders
61
73
  config.validate_config!
62
74
  @configurations ||= {}
63
75
  config.send(:cache_headers!)
76
+ config.send(:cache_hpkp_report_host)
64
77
  config.freeze
65
78
  @configurations[name] = config
66
79
  end
@@ -73,7 +86,6 @@ module SecureHeaders
73
86
  ALL_HEADER_CLASSES.each do |klass|
74
87
  config.send("#{klass::CONFIG_KEY}=", OPT_OUT)
75
88
  end
76
- config.dynamic_csp = OPT_OUT
77
89
  end
78
90
 
79
91
  add_configuration(NOOP_CONFIGURATION, noop_config)
@@ -82,6 +94,7 @@ module SecureHeaders
82
94
  # Public: perform a basic deep dup. The shallow copy provided by dup/clone
83
95
  # can lead to modifying parent objects.
84
96
  def deep_copy(config)
97
+ return unless config
85
98
  config.each_with_object({}) do |(key, value), hash|
86
99
  hash[key] = if value.is_a?(Array)
87
100
  value.dup
@@ -104,21 +117,41 @@ module SecureHeaders
104
117
 
105
118
  attr_writer :hsts, :x_frame_options, :x_content_type_options,
106
119
  :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
107
- :hpkp, :dynamic_csp, :cookies
120
+ :referrer_policy, :clear_site_data, :expect_certificate_transparency
121
+
122
+ attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
108
123
 
109
- attr_reader :cached_headers, :csp, :dynamic_csp, :cookies
124
+ @script_hashes = nil
125
+ @style_hashes = nil
110
126
 
111
127
  HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
112
- if File.exists?(HASH_CONFIG_FILE)
128
+ if File.exist?(HASH_CONFIG_FILE)
113
129
  config = YAML.safe_load(File.open(HASH_CONFIG_FILE))
114
130
  @script_hashes = config["scripts"]
115
131
  @style_hashes = config["styles"]
116
132
  end
117
133
 
118
134
  def initialize(&block)
135
+ @cookies = nil
136
+ @clear_site_data = nil
137
+ @csp = nil
138
+ @csp_report_only = nil
139
+ @hpkp_report_host = nil
140
+ @hpkp = nil
141
+ @hsts = nil
142
+ @x_content_type_options = nil
143
+ @x_download_options = nil
144
+ @x_frame_options = nil
145
+ @x_permitted_cross_domain_policies = nil
146
+ @x_xss_protection = nil
147
+ @expect_certificate_transparency = nil
148
+
119
149
  self.hpkp = OPT_OUT
120
- self.csp = self.class.send(:deep_copy, CSP::DEFAULT_CONFIG)
121
- instance_eval &block if block_given?
150
+ self.referrer_policy = OPT_OUT
151
+ self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT)
152
+ self.csp_report_only = OPT_OUT
153
+
154
+ instance_eval(&block) if block_given?
122
155
  end
123
156
 
124
157
  # Public: copy everything but the cached headers
@@ -126,9 +159,9 @@ module SecureHeaders
126
159
  # Returns a deep-dup'd copy of this configuration.
127
160
  def dup
128
161
  copy = self.class.new
129
- copy.cookies = @cookies
130
- copy.csp = self.class.send(:deep_copy_if_hash, @csp)
131
- copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp)
162
+ copy.cookies = self.class.send(:deep_copy_if_hash, @cookies)
163
+ copy.csp = @csp.dup if @csp
164
+ copy.csp_report_only = @csp_report_only.dup if @csp_report_only
132
165
  copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
133
166
  copy.x_content_type_options = @x_content_type_options
134
167
  copy.hsts = @hsts
@@ -136,15 +169,16 @@ module SecureHeaders
136
169
  copy.x_xss_protection = @x_xss_protection
137
170
  copy.x_download_options = @x_download_options
138
171
  copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
172
+ copy.clear_site_data = @clear_site_data
173
+ copy.expect_certificate_transparency = @expect_certificate_transparency
174
+ copy.referrer_policy = @referrer_policy
139
175
  copy.hpkp = @hpkp
176
+ copy.hpkp_report_host = @hpkp_report_host
140
177
  copy
141
178
  end
142
179
 
143
180
  def opt_out(header)
144
181
  send("#{header}=", OPT_OUT)
145
- if header == CSP::CONFIG_KEY
146
- dynamic_csp = OPT_OUT
147
- end
148
182
  self.cached_headers.delete(header)
149
183
  end
150
184
 
@@ -153,20 +187,6 @@ module SecureHeaders
153
187
  self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value)
154
188
  end
155
189
 
156
- # Public: generated cached headers for a specific user agent.
157
- def rebuild_csp_header_cache!(user_agent)
158
- self.cached_headers[CSP::CONFIG_KEY] = {}
159
- unless current_csp == OPT_OUT
160
- user_agent = UserAgent.parse(user_agent)
161
- variation = CSP.ua_to_variation(user_agent)
162
- self.cached_headers[CSP::CONFIG_KEY][variation] = CSP.make_header(current_csp, user_agent)
163
- end
164
- end
165
-
166
- def current_csp
167
- @dynamic_csp || @csp
168
- end
169
-
170
190
  # Public: validates all configurations values.
171
191
  #
172
192
  # Raises various configuration errors if any invalid config is detected.
@@ -175,37 +195,87 @@ module SecureHeaders
175
195
  def validate_config!
176
196
  StrictTransportSecurity.validate_config!(@hsts)
177
197
  ContentSecurityPolicy.validate_config!(@csp)
198
+ ContentSecurityPolicy.validate_config!(@csp_report_only)
199
+ ReferrerPolicy.validate_config!(@referrer_policy)
178
200
  XFrameOptions.validate_config!(@x_frame_options)
179
201
  XContentTypeOptions.validate_config!(@x_content_type_options)
180
202
  XXssProtection.validate_config!(@x_xss_protection)
181
203
  XDownloadOptions.validate_config!(@x_download_options)
182
204
  XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
205
+ ClearSiteData.validate_config!(@clear_site_data)
206
+ ExpectCertificateTransparency.validate_config!(@expect_certificate_transparency)
183
207
  PublicKeyPins.validate_config!(@hpkp)
184
208
  Cookie.validate_config!(@cookies)
185
209
  end
186
210
 
187
211
  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)
212
+ raise ArgumentError, "#{Kernel.caller.first}: `#secure_cookies=` is no longer supported. Please use `#cookies=` to configure secure cookies instead."
190
213
  end
191
214
 
192
- protected
193
-
194
215
  def csp=(new_csp)
195
- if self.dynamic_csp
196
- raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead."
216
+ if new_csp.respond_to?(:opt_out?)
217
+ @csp = new_csp.dup
218
+ else
219
+ if new_csp[:report_only]
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="
222
+ else
223
+ @csp = ContentSecurityPolicyConfig.new(new_csp)
224
+ end
197
225
  end
226
+ end
198
227
 
199
- @csp = new_csp
228
+ # Configures the Content-Security-Policy-Report-Only header. `new_csp` cannot
229
+ # contain `report_only: false` or an error will be raised.
230
+ #
231
+ # NOTE: if csp has not been configured/has the default value when
232
+ # configuring csp_report_only, the code will assume you mean to only use
233
+ # report-only mode and you will be opted-out of enforce mode.
234
+ def csp_report_only=(new_csp)
235
+ @csp_report_only = begin
236
+ if new_csp.is_a?(ContentSecurityPolicyConfig)
237
+ new_csp.make_report_only
238
+ elsif new_csp.respond_to?(:opt_out?)
239
+ new_csp.dup
240
+ else
241
+ if new_csp[:report_only] == false # nil is a valid value on which we do not want to raise
242
+ raise ContentSecurityPolicyConfigError, "`#csp_report_only=` was supplied a config with report_only: false. Use #csp="
243
+ else
244
+ ContentSecurityPolicyReportOnlyConfig.new(new_csp)
245
+ end
246
+ end
247
+ end
248
+ end
249
+
250
+ protected
251
+
252
+ def cookies=(cookies)
253
+ @cookies = cookies
200
254
  end
201
255
 
202
256
  def cached_headers=(headers)
203
257
  @cached_headers = headers
204
258
  end
205
259
 
260
+ def hpkp=(hpkp)
261
+ @hpkp = self.class.send(:deep_copy_if_hash, hpkp)
262
+ end
263
+
264
+ def hpkp_report_host=(hpkp_report_host)
265
+ @hpkp_report_host = hpkp_report_host
266
+ end
267
+
206
268
  private
207
269
 
208
- # Public: Precompute the header names and values for this configuraiton.
270
+ def cache_hpkp_report_host
271
+ has_report_uri = @hpkp && @hpkp != OPT_OUT && @hpkp[:report_uri]
272
+ self.hpkp_report_host = if has_report_uri
273
+ parsed_report_uri = URI.parse(@hpkp[:report_uri])
274
+ parsed_report_uri.host
275
+ end
276
+ end
277
+
278
+ # Public: Precompute the header names and values for this configuration.
209
279
  # Ensures that headers generated at configure time, not on demand.
210
280
  #
211
281
  # Returns the cached headers
@@ -231,12 +301,16 @@ module SecureHeaders
231
301
  #
232
302
  # Returns nothing
233
303
  def generate_csp_headers(headers)
234
- unless @csp == OPT_OUT
235
- headers[CSP::CONFIG_KEY] = {}
236
- csp_config = self.current_csp
237
- CSP::VARIATIONS.each do |name, _|
238
- csp = CSP.make_header(csp_config, UserAgent.parse(name))
239
- headers[CSP::CONFIG_KEY][name] = csp.freeze
304
+ generate_csp_headers_for_config(headers, ContentSecurityPolicyConfig::CONFIG_KEY, self.csp)
305
+ generate_csp_headers_for_config(headers, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY, self.csp_report_only)
306
+ end
307
+
308
+ def generate_csp_headers_for_config(headers, header_key, csp_config)
309
+ unless csp_config.opt_out?
310
+ headers[header_key] = {}
311
+ ContentSecurityPolicy::VARIATIONS.each do |name, _|
312
+ csp = ContentSecurityPolicy.make_header(csp_config, UserAgent.parse(name))
313
+ headers[header_key][name] = csp.freeze
240
314
  end
241
315
  end
242
316
  end
@@ -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
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+ module SecureHeaders
3
+ class ClearSiteDataConfigError < StandardError; end
4
+ class ClearSiteData
5
+ HEADER_NAME = "Clear-Site-Data".freeze
6
+
7
+ # Valid `types`
8
+ CACHE = "cache".freeze
9
+ COOKIES = "cookies".freeze
10
+ STORAGE = "storage".freeze
11
+ EXECUTION_CONTEXTS = "executionContexts".freeze
12
+ ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECUTION_CONTEXTS]
13
+
14
+ CONFIG_KEY = :clear_site_data
15
+
16
+ class << self
17
+ # Public: make an Clear-Site-Data header name, value pair
18
+ #
19
+ # Returns nil if not configured, returns header name and value if configured.
20
+ def make_header(config = nil)
21
+ case config
22
+ when nil, OPT_OUT, []
23
+ # noop
24
+ when Array
25
+ [HEADER_NAME, make_header_value(config)]
26
+ when true
27
+ [HEADER_NAME, make_header_value(ALL_TYPES)]
28
+ end
29
+ end
30
+
31
+ def validate_config!(config)
32
+ case config
33
+ when nil, OPT_OUT, true
34
+ # valid
35
+ when Array
36
+ unless config.all? { |t| t.is_a?(String) }
37
+ raise ClearSiteDataConfigError.new("types must be Strings")
38
+ end
39
+ else
40
+ raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`")
41
+ end
42
+ end
43
+
44
+ # Public: Transform a Clear-Site-Data config (an Array of Strings) into a
45
+ # String that can be used as the value for the Clear-Site-Data header.
46
+ #
47
+ # types - An Array of String of types of data to clear.
48
+ #
49
+ # Returns a String of quoted values that are comma separated.
50
+ def make_header_value(types)
51
+ types.map { |t| "\"#{t}\""}.join(", ")
52
+ end
53
+ end
54
+ end
55
+ end