secure_headers 3.2.0 → 3.8.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 +5 -5
- data/.github/ISSUE_TEMPLATE.md +41 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +2 -0
- data/CHANGELOG.md +160 -1
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +3 -1
- data/LICENSE +4 -199
- data/README.md +75 -334
- data/docs/HPKP.md +17 -0
- data/docs/cookies.md +51 -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 +120 -39
- data/lib/secure_headers/headers/clear_site_data.rb +54 -0
- data/lib/secure_headers/headers/content_security_policy.rb +134 -36
- data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
- data/lib/secure_headers/headers/cookie.rb +7 -1
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
- data/lib/secure_headers/headers/policy_management.rb +150 -66
- data/lib/secure_headers/headers/public_key_pins.rb +1 -1
- data/lib/secure_headers/headers/referrer_policy.rb +36 -0
- data/lib/secure_headers/headers/strict_transport_security.rb +1 -1
- data/lib/secure_headers/headers/x_content_type_options.rb +1 -1
- data/lib/secure_headers/headers/x_download_options.rb +1 -1
- data/lib/secure_headers/headers/x_frame_options.rb +1 -1
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +1 -1
- data/lib/secure_headers/headers/x_xss_protection.rb +1 -1
- data/lib/secure_headers/middleware.rb +6 -0
- data/lib/secure_headers/railtie.rb +1 -1
- data/lib/secure_headers/utils/cookies_config.rb +6 -4
- data/lib/secure_headers/view_helper.rb +13 -7
- data/lib/secure_headers.rb +138 -55
- data/lib/tasks/tasks.rake +5 -4
- data/secure_headers.gemspec +2 -2
- data/spec/lib/secure_headers/configuration_spec.rb +5 -5
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +86 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +96 -13
- data/spec/lib/secure_headers/headers/cookie_spec.rb +22 -25
- data/spec/lib/secure_headers/headers/expect_certificate_transparency_spec.rb +42 -0
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +59 -36
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +72 -0
- data/spec/lib/secure_headers/middleware_spec.rb +31 -1
- data/spec/lib/secure_headers/view_helpers_spec.rb +19 -7
- data/spec/lib/secure_headers_spec.rb +344 -44
- data/spec/spec_helper.rb +8 -3
- data/upgrading-to-3-0.md +13 -10
- metadata +24 -5
|
@@ -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
|
+
```
|
|
@@ -31,7 +31,7 @@ module SecureHeaders
|
|
|
31
31
|
raise NotYetConfiguredError, "#{base} policy not yet supplied"
|
|
32
32
|
end
|
|
33
33
|
override = @configurations[base].dup
|
|
34
|
-
override.instance_eval
|
|
34
|
+
override.instance_eval(&block) if block_given?
|
|
35
35
|
add_configuration(name, override)
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -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.
|
|
@@ -61,6 +72,7 @@ module SecureHeaders
|
|
|
61
72
|
config.validate_config!
|
|
62
73
|
@configurations ||= {}
|
|
63
74
|
config.send(:cache_headers!)
|
|
75
|
+
config.send(:cache_hpkp_report_host)
|
|
64
76
|
config.freeze
|
|
65
77
|
@configurations[name] = config
|
|
66
78
|
end
|
|
@@ -73,7 +85,6 @@ module SecureHeaders
|
|
|
73
85
|
ALL_HEADER_CLASSES.each do |klass|
|
|
74
86
|
config.send("#{klass::CONFIG_KEY}=", OPT_OUT)
|
|
75
87
|
end
|
|
76
|
-
config.dynamic_csp = OPT_OUT
|
|
77
88
|
end
|
|
78
89
|
|
|
79
90
|
add_configuration(NOOP_CONFIGURATION, noop_config)
|
|
@@ -82,6 +93,7 @@ module SecureHeaders
|
|
|
82
93
|
# Public: perform a basic deep dup. The shallow copy provided by dup/clone
|
|
83
94
|
# can lead to modifying parent objects.
|
|
84
95
|
def deep_copy(config)
|
|
96
|
+
return unless config
|
|
85
97
|
config.each_with_object({}) do |(key, value), hash|
|
|
86
98
|
hash[key] = if value.is_a?(Array)
|
|
87
99
|
value.dup
|
|
@@ -104,21 +116,41 @@ module SecureHeaders
|
|
|
104
116
|
|
|
105
117
|
attr_writer :hsts, :x_frame_options, :x_content_type_options,
|
|
106
118
|
:x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
|
|
107
|
-
:
|
|
119
|
+
:referrer_policy, :clear_site_data, :expect_certificate_transparency
|
|
120
|
+
|
|
121
|
+
attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
|
|
108
122
|
|
|
109
|
-
|
|
123
|
+
@script_hashes = nil
|
|
124
|
+
@style_hashes = nil
|
|
110
125
|
|
|
111
126
|
HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
|
|
112
|
-
if File.
|
|
127
|
+
if File.exist?(HASH_CONFIG_FILE)
|
|
113
128
|
config = YAML.safe_load(File.open(HASH_CONFIG_FILE))
|
|
114
129
|
@script_hashes = config["scripts"]
|
|
115
130
|
@style_hashes = config["styles"]
|
|
116
131
|
end
|
|
117
132
|
|
|
118
133
|
def initialize(&block)
|
|
134
|
+
@cookies = nil
|
|
135
|
+
@clear_site_data = nil
|
|
136
|
+
@csp = nil
|
|
137
|
+
@csp_report_only = nil
|
|
138
|
+
@hpkp_report_host = nil
|
|
139
|
+
@hpkp = nil
|
|
140
|
+
@hsts = nil
|
|
141
|
+
@x_content_type_options = nil
|
|
142
|
+
@x_download_options = nil
|
|
143
|
+
@x_frame_options = nil
|
|
144
|
+
@x_permitted_cross_domain_policies = nil
|
|
145
|
+
@x_xss_protection = nil
|
|
146
|
+
@expect_certificate_transparency = nil
|
|
147
|
+
|
|
119
148
|
self.hpkp = OPT_OUT
|
|
120
|
-
self.
|
|
121
|
-
|
|
149
|
+
self.referrer_policy = OPT_OUT
|
|
150
|
+
self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT)
|
|
151
|
+
self.csp_report_only = OPT_OUT
|
|
152
|
+
|
|
153
|
+
instance_eval(&block) if block_given?
|
|
122
154
|
end
|
|
123
155
|
|
|
124
156
|
# Public: copy everything but the cached headers
|
|
@@ -126,9 +158,9 @@ module SecureHeaders
|
|
|
126
158
|
# Returns a deep-dup'd copy of this configuration.
|
|
127
159
|
def dup
|
|
128
160
|
copy = self.class.new
|
|
129
|
-
copy.cookies = @cookies
|
|
130
|
-
copy.csp =
|
|
131
|
-
copy.
|
|
161
|
+
copy.cookies = self.class.send(:deep_copy_if_hash, @cookies)
|
|
162
|
+
copy.csp = @csp.dup if @csp
|
|
163
|
+
copy.csp_report_only = @csp_report_only.dup if @csp_report_only
|
|
132
164
|
copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
|
|
133
165
|
copy.x_content_type_options = @x_content_type_options
|
|
134
166
|
copy.hsts = @hsts
|
|
@@ -136,15 +168,16 @@ module SecureHeaders
|
|
|
136
168
|
copy.x_xss_protection = @x_xss_protection
|
|
137
169
|
copy.x_download_options = @x_download_options
|
|
138
170
|
copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
|
|
171
|
+
copy.clear_site_data = @clear_site_data
|
|
172
|
+
copy.expect_certificate_transparency = @expect_certificate_transparency
|
|
173
|
+
copy.referrer_policy = @referrer_policy
|
|
139
174
|
copy.hpkp = @hpkp
|
|
175
|
+
copy.hpkp_report_host = @hpkp_report_host
|
|
140
176
|
copy
|
|
141
177
|
end
|
|
142
178
|
|
|
143
179
|
def opt_out(header)
|
|
144
180
|
send("#{header}=", OPT_OUT)
|
|
145
|
-
if header == CSP::CONFIG_KEY
|
|
146
|
-
dynamic_csp = OPT_OUT
|
|
147
|
-
end
|
|
148
181
|
self.cached_headers.delete(header)
|
|
149
182
|
end
|
|
150
183
|
|
|
@@ -153,20 +186,6 @@ module SecureHeaders
|
|
|
153
186
|
self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value)
|
|
154
187
|
end
|
|
155
188
|
|
|
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
189
|
# Public: validates all configurations values.
|
|
171
190
|
#
|
|
172
191
|
# Raises various configuration errors if any invalid config is detected.
|
|
@@ -175,11 +194,15 @@ module SecureHeaders
|
|
|
175
194
|
def validate_config!
|
|
176
195
|
StrictTransportSecurity.validate_config!(@hsts)
|
|
177
196
|
ContentSecurityPolicy.validate_config!(@csp)
|
|
197
|
+
ContentSecurityPolicy.validate_config!(@csp_report_only)
|
|
198
|
+
ReferrerPolicy.validate_config!(@referrer_policy)
|
|
178
199
|
XFrameOptions.validate_config!(@x_frame_options)
|
|
179
200
|
XContentTypeOptions.validate_config!(@x_content_type_options)
|
|
180
201
|
XXssProtection.validate_config!(@x_xss_protection)
|
|
181
202
|
XDownloadOptions.validate_config!(@x_download_options)
|
|
182
203
|
XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
|
|
204
|
+
ClearSiteData.validate_config!(@clear_site_data)
|
|
205
|
+
ExpectCertificateTransparency.validate_config!(@expect_certificate_transparency)
|
|
183
206
|
PublicKeyPins.validate_config!(@hpkp)
|
|
184
207
|
Cookie.validate_config!(@cookies)
|
|
185
208
|
end
|
|
@@ -189,23 +212,77 @@ module SecureHeaders
|
|
|
189
212
|
@cookies = (@cookies || {}).merge(secure: secure_cookies)
|
|
190
213
|
end
|
|
191
214
|
|
|
192
|
-
protected
|
|
193
|
-
|
|
194
215
|
def csp=(new_csp)
|
|
195
|
-
if
|
|
196
|
-
|
|
216
|
+
if new_csp.respond_to?(:opt_out?)
|
|
217
|
+
@csp = new_csp.dup
|
|
218
|
+
else
|
|
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
|
|
224
|
+
else
|
|
225
|
+
@csp = ContentSecurityPolicyConfig.new(new_csp)
|
|
226
|
+
end
|
|
197
227
|
end
|
|
228
|
+
end
|
|
198
229
|
|
|
199
|
-
|
|
230
|
+
# Configures the Content-Security-Policy-Report-Only header. `new_csp` cannot
|
|
231
|
+
# contain `report_only: false` or an error will be raised.
|
|
232
|
+
#
|
|
233
|
+
# NOTE: if csp has not been configured/has the default value when
|
|
234
|
+
# configuring csp_report_only, the code will assume you mean to only use
|
|
235
|
+
# report-only mode and you will be opted-out of enforce mode.
|
|
236
|
+
def csp_report_only=(new_csp)
|
|
237
|
+
@csp_report_only = begin
|
|
238
|
+
if new_csp.is_a?(ContentSecurityPolicyConfig)
|
|
239
|
+
new_csp.make_report_only
|
|
240
|
+
elsif new_csp.respond_to?(:opt_out?)
|
|
241
|
+
new_csp.dup
|
|
242
|
+
else
|
|
243
|
+
if new_csp[:report_only] == false # nil is a valid value on which we do not want to raise
|
|
244
|
+
raise ContentSecurityPolicyConfigError, "`#csp_report_only=` was supplied a config with report_only: false. Use #csp="
|
|
245
|
+
else
|
|
246
|
+
ContentSecurityPolicyReportOnlyConfig.new(new_csp)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
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
|
+
end
|
|
256
|
+
|
|
257
|
+
protected
|
|
258
|
+
|
|
259
|
+
def cookies=(cookies)
|
|
260
|
+
@cookies = cookies
|
|
200
261
|
end
|
|
201
262
|
|
|
202
263
|
def cached_headers=(headers)
|
|
203
264
|
@cached_headers = headers
|
|
204
265
|
end
|
|
205
266
|
|
|
267
|
+
def hpkp=(hpkp)
|
|
268
|
+
@hpkp = self.class.send(:deep_copy_if_hash, hpkp)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def hpkp_report_host=(hpkp_report_host)
|
|
272
|
+
@hpkp_report_host = hpkp_report_host
|
|
273
|
+
end
|
|
274
|
+
|
|
206
275
|
private
|
|
207
276
|
|
|
208
|
-
|
|
277
|
+
def cache_hpkp_report_host
|
|
278
|
+
has_report_uri = @hpkp && @hpkp != OPT_OUT && @hpkp[:report_uri]
|
|
279
|
+
self.hpkp_report_host = if has_report_uri
|
|
280
|
+
parsed_report_uri = URI.parse(@hpkp[:report_uri])
|
|
281
|
+
parsed_report_uri.host
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Public: Precompute the header names and values for this configuration.
|
|
209
286
|
# Ensures that headers generated at configure time, not on demand.
|
|
210
287
|
#
|
|
211
288
|
# Returns the cached headers
|
|
@@ -231,12 +308,16 @@ module SecureHeaders
|
|
|
231
308
|
#
|
|
232
309
|
# Returns nothing
|
|
233
310
|
def generate_csp_headers(headers)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
311
|
+
generate_csp_headers_for_config(headers, ContentSecurityPolicyConfig::CONFIG_KEY, self.csp)
|
|
312
|
+
generate_csp_headers_for_config(headers, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY, self.csp_report_only)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def generate_csp_headers_for_config(headers, header_key, csp_config)
|
|
316
|
+
unless csp_config.opt_out?
|
|
317
|
+
headers[header_key] = {}
|
|
318
|
+
ContentSecurityPolicy::VARIATIONS.each do |name, _|
|
|
319
|
+
csp = ContentSecurityPolicy.make_header(csp_config, UserAgent.parse(name))
|
|
320
|
+
headers[header_key][name] = csp.freeze
|
|
240
321
|
end
|
|
241
322
|
end
|
|
242
323
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
class ClearSiteDataConfigError < StandardError; end
|
|
3
|
+
class ClearSiteData
|
|
4
|
+
HEADER_NAME = "Clear-Site-Data".freeze
|
|
5
|
+
|
|
6
|
+
# Valid `types`
|
|
7
|
+
CACHE = "cache".freeze
|
|
8
|
+
COOKIES = "cookies".freeze
|
|
9
|
+
STORAGE = "storage".freeze
|
|
10
|
+
EXECTION_CONTEXTS = "executionContexts".freeze
|
|
11
|
+
ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECTION_CONTEXTS]
|
|
12
|
+
|
|
13
|
+
CONFIG_KEY = :clear_site_data
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Public: make an Clear-Site-Data header name, value pair
|
|
17
|
+
#
|
|
18
|
+
# Returns nil if not configured, returns header name and value if configured.
|
|
19
|
+
def make_header(config=nil)
|
|
20
|
+
case config
|
|
21
|
+
when nil, OPT_OUT, []
|
|
22
|
+
# noop
|
|
23
|
+
when Array
|
|
24
|
+
[HEADER_NAME, make_header_value(config)]
|
|
25
|
+
when true
|
|
26
|
+
[HEADER_NAME, make_header_value(ALL_TYPES)]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def validate_config!(config)
|
|
31
|
+
case config
|
|
32
|
+
when nil, OPT_OUT, true
|
|
33
|
+
# valid
|
|
34
|
+
when Array
|
|
35
|
+
unless config.all? { |t| t.is_a?(String) }
|
|
36
|
+
raise ClearSiteDataConfigError.new("types must be Strings")
|
|
37
|
+
end
|
|
38
|
+
else
|
|
39
|
+
raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Public: Transform a Clear-Site-Data config (an Array of Strings) into a
|
|
44
|
+
# String that can be used as the value for the Clear-Site-Data header.
|
|
45
|
+
#
|
|
46
|
+
# types - An Array of String of types of data to clear.
|
|
47
|
+
#
|
|
48
|
+
# Returns a String of quoted values that are comma separated.
|
|
49
|
+
def make_header_value(types)
|
|
50
|
+
types.map { |t| "\"#{t}\""}.join(", ")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|