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.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE.md +41 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- data/.rspec +2 -0
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -1
- data/.travis.yml +8 -4
- data/CHANGELOG.md +144 -1
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +9 -4
- data/Guardfile +1 -0
- data/LICENSE +4 -199
- data/README.md +73 -344
- data/Rakefile +22 -18
- data/docs/HPKP.md +17 -0
- data/docs/cookies.md +64 -0
- data/docs/hashes.md +64 -0
- data/docs/named_overrides_and_appends.md +107 -0
- data/docs/per_action_configuration.md +133 -0
- data/docs/sinatra.md +25 -0
- data/lib/secure_headers/configuration.rb +116 -42
- data/lib/secure_headers/hash_helper.rb +2 -1
- data/lib/secure_headers/headers/clear_site_data.rb +55 -0
- data/lib/secure_headers/headers/content_security_policy.rb +128 -39
- data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
- data/lib/secure_headers/headers/cookie.rb +21 -3
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
- data/lib/secure_headers/headers/policy_management.rb +158 -68
- data/lib/secure_headers/headers/public_key_pins.rb +5 -4
- data/lib/secure_headers/headers/referrer_policy.rb +37 -0
- data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
- data/lib/secure_headers/headers/x_content_type_options.rb +2 -1
- data/lib/secure_headers/headers/x_download_options.rb +3 -2
- data/lib/secure_headers/headers/x_frame_options.rb +2 -1
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
- data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
- data/lib/secure_headers/middleware.rb +16 -9
- data/lib/secure_headers/railtie.rb +7 -6
- data/lib/secure_headers/utils/cookies_config.rb +13 -12
- data/lib/secure_headers/view_helper.rb +15 -8
- data/lib/secure_headers.rb +139 -55
- data/lib/tasks/tasks.rake +7 -5
- data/secure_headers.gemspec +13 -3
- data/spec/lib/secure_headers/configuration_spec.rb +13 -12
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +89 -15
- data/spec/lib/secure_headers/headers/cookie_spec.rb +38 -20
- data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +80 -42
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +73 -0
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +5 -4
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
- data/spec/lib/secure_headers/middleware_spec.rb +55 -17
- data/spec/lib/secure_headers/view_helpers_spec.rb +21 -8
- data/spec/lib/secure_headers_spec.rb +330 -58
- data/spec/spec_helper.rb +17 -25
- data/upgrading-to-3-0.md +13 -10
- data/upgrading-to-4-0.md +55 -0
- 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
|
-
|
|
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
|
|
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
|
-
:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
121
|
-
|
|
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 =
|
|
131
|
-
copy.
|
|
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
|
-
|
|
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
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
@@ -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
|