secure_headers 3.4.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +75 -0
- data/Gemfile +1 -0
- data/README.md +54 -322
- data/docs/HPKP.md +17 -0
- data/docs/cookies.md +50 -0
- data/docs/hashes.md +64 -0
- data/docs/named_overrides_and_appends.md +107 -0
- data/docs/per_action_configuration.md +105 -0
- data/docs/sinatra.md +25 -0
- data/lib/secure_headers/configuration.rb +71 -37
- data/lib/secure_headers/headers/clear_site_data.rb +51 -0
- data/lib/secure_headers/headers/content_security_policy.rb +64 -37
- data/lib/secure_headers/headers/content_security_policy_config.rb +128 -0
- data/lib/secure_headers/headers/policy_management.rb +49 -30
- data/lib/secure_headers/headers/referrer_policy.rb +3 -0
- data/lib/secure_headers/view_helper.rb +8 -0
- data/lib/secure_headers.rb +133 -55
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/configuration_spec.rb +5 -5
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +103 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +14 -2
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +25 -34
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +18 -0
- data/spec/lib/secure_headers/middleware_spec.rb +13 -1
- data/spec/lib/secure_headers/view_helpers_spec.rb +19 -6
- data/spec/lib/secure_headers_spec.rb +289 -40
- data/spec/spec_helper.rb +3 -2
- metadata +12 -2
|
@@ -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,105 @@
|
|
|
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
|
+
```
|
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
|
+
```
|
|
@@ -46,6 +46,17 @@ module SecureHeaders
|
|
|
46
46
|
@configurations[name]
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
def named_appends(name)
|
|
50
|
+
@appends ||= {}
|
|
51
|
+
@appends[name]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def named_append(name, target = nil, &block)
|
|
55
|
+
@appends ||= {}
|
|
56
|
+
raise "Provide a configuration block" unless block_given?
|
|
57
|
+
@appends[name] = block
|
|
58
|
+
end
|
|
59
|
+
|
|
49
60
|
private
|
|
50
61
|
|
|
51
62
|
# Private: add a valid configuration to the global set of named configs.
|
|
@@ -74,7 +85,6 @@ module SecureHeaders
|
|
|
74
85
|
ALL_HEADER_CLASSES.each do |klass|
|
|
75
86
|
config.send("#{klass::CONFIG_KEY}=", OPT_OUT)
|
|
76
87
|
end
|
|
77
|
-
config.dynamic_csp = OPT_OUT
|
|
78
88
|
end
|
|
79
89
|
|
|
80
90
|
add_configuration(NOOP_CONFIGURATION, noop_config)
|
|
@@ -83,6 +93,7 @@ module SecureHeaders
|
|
|
83
93
|
# Public: perform a basic deep dup. The shallow copy provided by dup/clone
|
|
84
94
|
# can lead to modifying parent objects.
|
|
85
95
|
def deep_copy(config)
|
|
96
|
+
return unless config
|
|
86
97
|
config.each_with_object({}) do |(key, value), hash|
|
|
87
98
|
hash[key] = if value.is_a?(Array)
|
|
88
99
|
value.dup
|
|
@@ -103,13 +114,11 @@ module SecureHeaders
|
|
|
103
114
|
end
|
|
104
115
|
end
|
|
105
116
|
|
|
106
|
-
attr_accessor :dynamic_csp
|
|
107
|
-
|
|
108
117
|
attr_writer :hsts, :x_frame_options, :x_content_type_options,
|
|
109
118
|
:x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
|
|
110
|
-
:referrer_policy
|
|
119
|
+
:referrer_policy, :clear_site_data
|
|
111
120
|
|
|
112
|
-
attr_reader :cached_headers, :csp, :cookies, :hpkp, :hpkp_report_host
|
|
121
|
+
attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
|
|
113
122
|
|
|
114
123
|
HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
|
|
115
124
|
if File.exists?(HASH_CONFIG_FILE)
|
|
@@ -121,7 +130,8 @@ module SecureHeaders
|
|
|
121
130
|
def initialize(&block)
|
|
122
131
|
self.hpkp = OPT_OUT
|
|
123
132
|
self.referrer_policy = OPT_OUT
|
|
124
|
-
self.csp =
|
|
133
|
+
self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT)
|
|
134
|
+
self.csp_report_only = OPT_OUT
|
|
125
135
|
instance_eval &block if block_given?
|
|
126
136
|
end
|
|
127
137
|
|
|
@@ -130,9 +140,9 @@ module SecureHeaders
|
|
|
130
140
|
# Returns a deep-dup'd copy of this configuration.
|
|
131
141
|
def dup
|
|
132
142
|
copy = self.class.new
|
|
133
|
-
copy.cookies = @cookies
|
|
134
|
-
copy.csp =
|
|
135
|
-
copy.
|
|
143
|
+
copy.cookies = self.class.send(:deep_copy_if_hash, @cookies)
|
|
144
|
+
copy.csp = @csp.dup if @csp
|
|
145
|
+
copy.csp_report_only = @csp_report_only.dup if @csp_report_only
|
|
136
146
|
copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
|
|
137
147
|
copy.x_content_type_options = @x_content_type_options
|
|
138
148
|
copy.hsts = @hsts
|
|
@@ -140,6 +150,7 @@ module SecureHeaders
|
|
|
140
150
|
copy.x_xss_protection = @x_xss_protection
|
|
141
151
|
copy.x_download_options = @x_download_options
|
|
142
152
|
copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
|
|
153
|
+
copy.clear_site_data = @clear_site_data
|
|
143
154
|
copy.referrer_policy = @referrer_policy
|
|
144
155
|
copy.hpkp = @hpkp
|
|
145
156
|
copy.hpkp_report_host = @hpkp_report_host
|
|
@@ -148,9 +159,6 @@ module SecureHeaders
|
|
|
148
159
|
|
|
149
160
|
def opt_out(header)
|
|
150
161
|
send("#{header}=", OPT_OUT)
|
|
151
|
-
if header == CSP::CONFIG_KEY
|
|
152
|
-
dynamic_csp = OPT_OUT
|
|
153
|
-
end
|
|
154
162
|
self.cached_headers.delete(header)
|
|
155
163
|
end
|
|
156
164
|
|
|
@@ -159,20 +167,6 @@ module SecureHeaders
|
|
|
159
167
|
self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value)
|
|
160
168
|
end
|
|
161
169
|
|
|
162
|
-
# Public: generated cached headers for a specific user agent.
|
|
163
|
-
def rebuild_csp_header_cache!(user_agent)
|
|
164
|
-
self.cached_headers[CSP::CONFIG_KEY] = {}
|
|
165
|
-
unless current_csp == OPT_OUT
|
|
166
|
-
user_agent = UserAgent.parse(user_agent)
|
|
167
|
-
variation = CSP.ua_to_variation(user_agent)
|
|
168
|
-
self.cached_headers[CSP::CONFIG_KEY][variation] = CSP.make_header(current_csp, user_agent)
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def current_csp
|
|
173
|
-
@dynamic_csp || @csp
|
|
174
|
-
end
|
|
175
|
-
|
|
176
170
|
# Public: validates all configurations values.
|
|
177
171
|
#
|
|
178
172
|
# Raises various configuration errors if any invalid config is detected.
|
|
@@ -181,12 +175,14 @@ module SecureHeaders
|
|
|
181
175
|
def validate_config!
|
|
182
176
|
StrictTransportSecurity.validate_config!(@hsts)
|
|
183
177
|
ContentSecurityPolicy.validate_config!(@csp)
|
|
178
|
+
ContentSecurityPolicy.validate_config!(@csp_report_only)
|
|
184
179
|
ReferrerPolicy.validate_config!(@referrer_policy)
|
|
185
180
|
XFrameOptions.validate_config!(@x_frame_options)
|
|
186
181
|
XContentTypeOptions.validate_config!(@x_content_type_options)
|
|
187
182
|
XXssProtection.validate_config!(@x_xss_protection)
|
|
188
183
|
XDownloadOptions.validate_config!(@x_download_options)
|
|
189
184
|
XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
|
|
185
|
+
ClearSiteData.validate_config!(@clear_site_data)
|
|
190
186
|
PublicKeyPins.validate_config!(@hpkp)
|
|
191
187
|
Cookie.validate_config!(@cookies)
|
|
192
188
|
end
|
|
@@ -196,16 +192,50 @@ module SecureHeaders
|
|
|
196
192
|
@cookies = (@cookies || {}).merge(secure: secure_cookies)
|
|
197
193
|
end
|
|
198
194
|
|
|
199
|
-
protected
|
|
200
|
-
|
|
201
195
|
def csp=(new_csp)
|
|
202
|
-
if
|
|
203
|
-
|
|
196
|
+
if new_csp.respond_to?(:opt_out?)
|
|
197
|
+
@csp = new_csp.dup
|
|
198
|
+
else
|
|
199
|
+
if new_csp[:report_only]
|
|
200
|
+
# Deprecated configuration implies that CSPRO should be set, CSP should not - so opt out
|
|
201
|
+
Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp=` was supplied a config with report_only: true. Use #csp_report_only="
|
|
202
|
+
@csp = OPT_OUT
|
|
203
|
+
self.csp_report_only = new_csp
|
|
204
|
+
else
|
|
205
|
+
@csp = ContentSecurityPolicyConfig.new(new_csp)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Configures the Content-Security-Policy-Report-Only header. `new_csp` cannot
|
|
211
|
+
# contain `report_only: false` or an error will be raised.
|
|
212
|
+
#
|
|
213
|
+
# NOTE: if csp has not been configured/has the default value when
|
|
214
|
+
# configuring csp_report_only, the code will assume you mean to only use
|
|
215
|
+
# report-only mode and you will be opted-out of enforce mode.
|
|
216
|
+
def csp_report_only=(new_csp)
|
|
217
|
+
@csp_report_only = begin
|
|
218
|
+
if new_csp.is_a?(ContentSecurityPolicyConfig)
|
|
219
|
+
new_csp.make_report_only
|
|
220
|
+
elsif new_csp.respond_to?(:opt_out?)
|
|
221
|
+
new_csp.dup
|
|
222
|
+
else
|
|
223
|
+
if new_csp[:report_only] == false # nil is a valid value on which we do not want to raise
|
|
224
|
+
raise ContentSecurityPolicyConfigError, "`#csp_report_only=` was supplied a config with report_only: false. Use #csp="
|
|
225
|
+
else
|
|
226
|
+
ContentSecurityPolicyReportOnlyConfig.new(new_csp)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
204
229
|
end
|
|
205
230
|
|
|
206
|
-
@csp
|
|
231
|
+
if !@csp_report_only.opt_out? && @csp.to_h == ContentSecurityPolicyConfig::DEFAULT
|
|
232
|
+
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="
|
|
233
|
+
@csp = OPT_OUT
|
|
234
|
+
end
|
|
207
235
|
end
|
|
208
236
|
|
|
237
|
+
protected
|
|
238
|
+
|
|
209
239
|
def cookies=(cookies)
|
|
210
240
|
@cookies = cookies
|
|
211
241
|
end
|
|
@@ -258,12 +288,16 @@ module SecureHeaders
|
|
|
258
288
|
#
|
|
259
289
|
# Returns nothing
|
|
260
290
|
def generate_csp_headers(headers)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
291
|
+
generate_csp_headers_for_config(headers, ContentSecurityPolicyConfig::CONFIG_KEY, self.csp)
|
|
292
|
+
generate_csp_headers_for_config(headers, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY, self.csp_report_only)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def generate_csp_headers_for_config(headers, header_key, csp_config)
|
|
296
|
+
unless csp_config.opt_out?
|
|
297
|
+
headers[header_key] = {}
|
|
298
|
+
ContentSecurityPolicy::VARIATIONS.each do |name, _|
|
|
299
|
+
csp = ContentSecurityPolicy.make_header(csp_config, UserAgent.parse(name))
|
|
300
|
+
headers[header_key][name] = csp.freeze
|
|
267
301
|
end
|
|
268
302
|
end
|
|
269
303
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
class ClearSiteDataConfigError < StandardError; end
|
|
3
|
+
class ClearSiteData
|
|
4
|
+
HEADER_NAME = "Clear-Site-Data".freeze
|
|
5
|
+
TYPES = "types".freeze
|
|
6
|
+
|
|
7
|
+
# Valid `types`
|
|
8
|
+
CACHE = "cache".freeze
|
|
9
|
+
COOKIES = "cookies".freeze
|
|
10
|
+
STORAGE = "storage".freeze
|
|
11
|
+
EXECTION_CONTEXTS = "executionContexts".freeze
|
|
12
|
+
ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECTION_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, JSON.dump(TYPES => config)]
|
|
26
|
+
when true
|
|
27
|
+
[HEADER_NAME, JSON.dump(TYPES => 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
|
+
|
|
40
|
+
begin
|
|
41
|
+
JSON.dump(config)
|
|
42
|
+
rescue JSON::GeneratorError, Encoding::UndefinedConversionError
|
|
43
|
+
raise ClearSiteDataConfigError.new("types must serializable by JSON")
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|