secure_headers 3.5.1 → 3.6.7
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/.rspec +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +2 -0
- data/CHANGELOG.md +34 -1
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +41 -0
- data/LICENSE +4 -199
- data/README.md +36 -384
- 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 +24 -5
- data/lib/secure_headers/headers/clear_site_data.rb +54 -0
- data/lib/secure_headers/headers/content_security_policy.rb +2 -2
- data/lib/secure_headers/headers/content_security_policy_config.rb +45 -12
- data/lib/secure_headers/headers/policy_management.rb +22 -22
- data/lib/secure_headers/headers/public_key_pins.rb +1 -1
- data/lib/secure_headers/headers/referrer_policy.rb +1 -1
- 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/view_helper.rb +2 -2
- data/lib/secure_headers.rb +5 -2
- data/secure_headers.gemspec +2 -2
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +86 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +12 -8
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +1 -0
- data/spec/lib/secure_headers/view_helpers_spec.rb +0 -1
- data/spec/lib/secure_headers_spec.rb +39 -4
- metadata +15 -4
|
@@ -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
|
|
|
@@ -116,23 +116,40 @@ module SecureHeaders
|
|
|
116
116
|
|
|
117
117
|
attr_writer :hsts, :x_frame_options, :x_content_type_options,
|
|
118
118
|
:x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
|
|
119
|
-
:referrer_policy
|
|
119
|
+
:referrer_policy, :clear_site_data
|
|
120
120
|
|
|
121
121
|
attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
|
|
122
122
|
|
|
123
|
+
@script_hashes = nil
|
|
124
|
+
@style_hashes = nil
|
|
125
|
+
|
|
123
126
|
HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
|
|
124
|
-
if File.
|
|
127
|
+
if File.exist?(HASH_CONFIG_FILE)
|
|
125
128
|
config = YAML.safe_load(File.open(HASH_CONFIG_FILE))
|
|
126
129
|
@script_hashes = config["scripts"]
|
|
127
130
|
@style_hashes = config["styles"]
|
|
128
131
|
end
|
|
129
132
|
|
|
130
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
|
+
|
|
131
147
|
self.hpkp = OPT_OUT
|
|
132
148
|
self.referrer_policy = OPT_OUT
|
|
133
149
|
self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT)
|
|
134
150
|
self.csp_report_only = OPT_OUT
|
|
135
|
-
|
|
151
|
+
|
|
152
|
+
instance_eval(&block) if block_given?
|
|
136
153
|
end
|
|
137
154
|
|
|
138
155
|
# Public: copy everything but the cached headers
|
|
@@ -150,6 +167,7 @@ module SecureHeaders
|
|
|
150
167
|
copy.x_xss_protection = @x_xss_protection
|
|
151
168
|
copy.x_download_options = @x_download_options
|
|
152
169
|
copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
|
|
170
|
+
copy.clear_site_data = @clear_site_data
|
|
153
171
|
copy.referrer_policy = @referrer_policy
|
|
154
172
|
copy.hpkp = @hpkp
|
|
155
173
|
copy.hpkp_report_host = @hpkp_report_host
|
|
@@ -181,6 +199,7 @@ module SecureHeaders
|
|
|
181
199
|
XXssProtection.validate_config!(@x_xss_protection)
|
|
182
200
|
XDownloadOptions.validate_config!(@x_download_options)
|
|
183
201
|
XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
|
|
202
|
+
ClearSiteData.validate_config!(@clear_site_data)
|
|
184
203
|
PublicKeyPins.validate_config!(@hpkp)
|
|
185
204
|
Cookie.validate_config!(@cookies)
|
|
186
205
|
end
|
|
@@ -260,7 +279,7 @@ module SecureHeaders
|
|
|
260
279
|
end
|
|
261
280
|
end
|
|
262
281
|
|
|
263
|
-
# Public: Precompute the header names and values for this
|
|
282
|
+
# Public: Precompute the header names and values for this configuration.
|
|
264
283
|
# Ensures that headers generated at configure time, not on demand.
|
|
265
284
|
#
|
|
266
285
|
# Returns the cached headers
|
|
@@ -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
|
|
@@ -59,8 +59,6 @@ module SecureHeaders
|
|
|
59
59
|
def normalize_child_frame_src
|
|
60
60
|
if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src
|
|
61
61
|
Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers.")
|
|
62
|
-
elsif @config.frame_src
|
|
63
|
-
Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config.frame_src}.")
|
|
64
62
|
end
|
|
65
63
|
|
|
66
64
|
if supported_directives.include?(:child_src)
|
|
@@ -216,6 +214,8 @@ module SecureHeaders
|
|
|
216
214
|
@supported_directives ||= if VARIATIONS[@parsed_ua.browser]
|
|
217
215
|
if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
|
|
218
216
|
VARIATIONS["FirefoxTransitional"]
|
|
217
|
+
elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10)
|
|
218
|
+
VARIATIONS["SafariTransitional"]
|
|
219
219
|
else
|
|
220
220
|
VARIATIONS[@parsed_ua.browser]
|
|
221
221
|
end
|
|
@@ -6,12 +6,7 @@ module SecureHeaders
|
|
|
6
6
|
base.attrs.each do |attr|
|
|
7
7
|
base.send(:define_method, "#{attr}=") do |value|
|
|
8
8
|
if self.class.attrs.include?(attr)
|
|
9
|
-
|
|
10
|
-
prev_value = self.instance_variable_get("@#{attr}")
|
|
11
|
-
self.instance_variable_set("@#{attr}", value)
|
|
12
|
-
if prev_value != self.instance_variable_get("@#{attr}")
|
|
13
|
-
@modified = true
|
|
14
|
-
end
|
|
9
|
+
write_attribute(attr, value)
|
|
15
10
|
else
|
|
16
11
|
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}"
|
|
17
12
|
end
|
|
@@ -20,6 +15,30 @@ module SecureHeaders
|
|
|
20
15
|
end
|
|
21
16
|
|
|
22
17
|
def initialize(hash)
|
|
18
|
+
@base_uri = nil
|
|
19
|
+
@block_all_mixed_content = nil
|
|
20
|
+
@child_src = nil
|
|
21
|
+
@connect_src = nil
|
|
22
|
+
@default_src = nil
|
|
23
|
+
@font_src = nil
|
|
24
|
+
@form_action = nil
|
|
25
|
+
@frame_ancestors = nil
|
|
26
|
+
@frame_src = nil
|
|
27
|
+
@img_src = nil
|
|
28
|
+
@manifest_src = nil
|
|
29
|
+
@media_src = nil
|
|
30
|
+
@object_src = nil
|
|
31
|
+
@plugin_types = nil
|
|
32
|
+
@preserve_schemes = nil
|
|
33
|
+
@report_only = nil
|
|
34
|
+
@report_uri = nil
|
|
35
|
+
@sandbox = nil
|
|
36
|
+
@script_nonce = nil
|
|
37
|
+
@script_src = nil
|
|
38
|
+
@style_nonce = nil
|
|
39
|
+
@style_src = nil
|
|
40
|
+
@upgrade_insecure_requests = nil
|
|
41
|
+
|
|
23
42
|
from_hash(hash)
|
|
24
43
|
@modified = false
|
|
25
44
|
end
|
|
@@ -52,8 +71,9 @@ module SecureHeaders
|
|
|
52
71
|
|
|
53
72
|
def to_h
|
|
54
73
|
self.class.attrs.each_with_object({}) do |key, hash|
|
|
55
|
-
|
|
56
|
-
|
|
74
|
+
value = self.send(key)
|
|
75
|
+
hash[key] = value unless value.nil?
|
|
76
|
+
end
|
|
57
77
|
end
|
|
58
78
|
|
|
59
79
|
def dup
|
|
@@ -73,14 +93,26 @@ module SecureHeaders
|
|
|
73
93
|
|
|
74
94
|
private
|
|
75
95
|
def from_hash(hash)
|
|
76
|
-
hash.
|
|
96
|
+
hash.each_pair do |k, v|
|
|
97
|
+
next if v.nil?
|
|
98
|
+
|
|
77
99
|
if self.class.attrs.include?(k)
|
|
78
|
-
|
|
100
|
+
write_attribute(k, v)
|
|
79
101
|
else
|
|
80
|
-
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{
|
|
102
|
+
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{v}"
|
|
81
103
|
end
|
|
82
104
|
end
|
|
83
105
|
end
|
|
106
|
+
|
|
107
|
+
def write_attribute(attr, value)
|
|
108
|
+
value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
|
|
109
|
+
attr_variable = "@#{attr}"
|
|
110
|
+
prev_value = self.instance_variable_get(attr_variable)
|
|
111
|
+
self.instance_variable_set(attr_variable, value)
|
|
112
|
+
if prev_value != value
|
|
113
|
+
@modified = true
|
|
114
|
+
end
|
|
115
|
+
end
|
|
84
116
|
end
|
|
85
117
|
|
|
86
118
|
class ContentSecurityPolicyConfigError < StandardError; end
|
|
@@ -88,8 +120,9 @@ module SecureHeaders
|
|
|
88
120
|
CONFIG_KEY = :csp
|
|
89
121
|
HEADER_NAME = "Content-Security-Policy".freeze
|
|
90
122
|
|
|
123
|
+
ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
|
|
91
124
|
def self.attrs
|
|
92
|
-
|
|
125
|
+
ATTRS
|
|
93
126
|
end
|
|
94
127
|
|
|
95
128
|
include DynamicConfig
|
|
@@ -62,25 +62,19 @@ module SecureHeaders
|
|
|
62
62
|
|
|
63
63
|
# All the directives currently under consideration for CSP level 3.
|
|
64
64
|
# https://w3c.github.io/webappsec/specs/CSP2/
|
|
65
|
+
BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
|
|
65
66
|
MANIFEST_SRC = :manifest_src
|
|
66
|
-
|
|
67
|
+
UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
|
|
67
68
|
DIRECTIVES_3_0 = [
|
|
68
69
|
DIRECTIVES_2_0,
|
|
69
|
-
MANIFEST_SRC,
|
|
70
|
-
REFLECTED_XSS
|
|
71
|
-
].flatten.freeze
|
|
72
|
-
|
|
73
|
-
# All the directives that are not currently in a formal spec, but have
|
|
74
|
-
# been implemented somewhere.
|
|
75
|
-
BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
|
|
76
|
-
UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
|
|
77
|
-
DIRECTIVES_DRAFT = [
|
|
78
70
|
BLOCK_ALL_MIXED_CONTENT,
|
|
71
|
+
MANIFEST_SRC,
|
|
79
72
|
UPGRADE_INSECURE_REQUESTS
|
|
80
|
-
].freeze
|
|
73
|
+
].flatten.freeze
|
|
81
74
|
|
|
82
75
|
EDGE_DIRECTIVES = DIRECTIVES_1_0
|
|
83
76
|
SAFARI_DIRECTIVES = DIRECTIVES_1_0
|
|
77
|
+
SAFARI_10_DIRECTIVES = DIRECTIVES_2_0
|
|
84
78
|
|
|
85
79
|
FIREFOX_UNSUPPORTED_DIRECTIVES = [
|
|
86
80
|
BLOCK_ALL_MIXED_CONTENT,
|
|
@@ -98,18 +92,18 @@ module SecureHeaders
|
|
|
98
92
|
].freeze
|
|
99
93
|
|
|
100
94
|
FIREFOX_DIRECTIVES = (
|
|
101
|
-
|
|
95
|
+
DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
|
|
102
96
|
).freeze
|
|
103
97
|
|
|
104
98
|
FIREFOX_46_DIRECTIVES = (
|
|
105
|
-
|
|
99
|
+
DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
|
|
106
100
|
).freeze
|
|
107
101
|
|
|
108
102
|
CHROME_DIRECTIVES = (
|
|
109
|
-
|
|
103
|
+
DIRECTIVES_3_0
|
|
110
104
|
).freeze
|
|
111
105
|
|
|
112
|
-
ALL_DIRECTIVES =
|
|
106
|
+
ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort
|
|
113
107
|
|
|
114
108
|
# Think of default-src and report-uri as the beginning and end respectively,
|
|
115
109
|
# everything else is in between.
|
|
@@ -133,6 +127,7 @@ module SecureHeaders
|
|
|
133
127
|
"Firefox" => FIREFOX_DIRECTIVES,
|
|
134
128
|
"FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
|
|
135
129
|
"Safari" => SAFARI_DIRECTIVES,
|
|
130
|
+
"SafariTransitional" => SAFARI_10_DIRECTIVES,
|
|
136
131
|
"Edge" => EDGE_DIRECTIVES,
|
|
137
132
|
"Other" => CHROME_DIRECTIVES
|
|
138
133
|
}.freeze
|
|
@@ -154,7 +149,6 @@ module SecureHeaders
|
|
|
154
149
|
MEDIA_SRC => :source_list,
|
|
155
150
|
OBJECT_SRC => :source_list,
|
|
156
151
|
PLUGIN_TYPES => :source_list,
|
|
157
|
-
REFLECTED_XSS => :string,
|
|
158
152
|
REPORT_URI => :source_list,
|
|
159
153
|
SANDBOX => :source_list,
|
|
160
154
|
SCRIPT_SRC => :source_list,
|
|
@@ -271,20 +265,26 @@ module SecureHeaders
|
|
|
271
265
|
# copy the default-src value to the original config. This modifies the original hash.
|
|
272
266
|
def populate_fetch_source_with_default!(original, additions)
|
|
273
267
|
# in case we would be appending to an empty directive, fill it with the default-src value
|
|
274
|
-
additions.
|
|
268
|
+
additions.each_key do |directive|
|
|
275
269
|
if !original[directive] && ((source_list?(directive) && FETCH_SOURCES.include?(directive)) || nonce_added?(original, additions))
|
|
276
270
|
if nonce_added?(original, additions)
|
|
277
271
|
inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym
|
|
278
272
|
unless original[inferred_directive] || NON_FETCH_SOURCES.include?(inferred_directive)
|
|
279
|
-
original[inferred_directive] = original
|
|
273
|
+
original[inferred_directive] = default_for(directive, original)
|
|
280
274
|
end
|
|
281
275
|
else
|
|
282
|
-
original[directive] = original
|
|
276
|
+
original[directive] = default_for(directive, original)
|
|
283
277
|
end
|
|
284
278
|
end
|
|
285
279
|
end
|
|
286
280
|
end
|
|
287
281
|
|
|
282
|
+
def default_for(directive, original)
|
|
283
|
+
return original[FRAME_SRC] if directive == CHILD_SRC && original[FRAME_SRC]
|
|
284
|
+
return original[CHILD_SRC] if directive == FRAME_SRC && original[CHILD_SRC]
|
|
285
|
+
original[DEFAULT_SRC]
|
|
286
|
+
end
|
|
287
|
+
|
|
288
288
|
def nonce_added?(original, additions)
|
|
289
289
|
[:script_nonce, :style_nonce].each do |nonce|
|
|
290
290
|
if additions[nonce] && !original[nonce]
|
|
@@ -340,9 +340,9 @@ module SecureHeaders
|
|
|
340
340
|
end
|
|
341
341
|
|
|
342
342
|
def ensure_valid_sources!(directive, source_expression)
|
|
343
|
-
source_expression.each do |
|
|
344
|
-
if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(
|
|
345
|
-
raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{
|
|
343
|
+
source_expression.each do |expression|
|
|
344
|
+
if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(expression)
|
|
345
|
+
raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{expression}). This value must be single quoted.")
|
|
346
346
|
end
|
|
347
347
|
end
|
|
348
348
|
end
|
|
@@ -2,7 +2,7 @@ module SecureHeaders
|
|
|
2
2
|
class STSConfigError < StandardError; end
|
|
3
3
|
|
|
4
4
|
class StrictTransportSecurity
|
|
5
|
-
HEADER_NAME = 'Strict-Transport-Security'
|
|
5
|
+
HEADER_NAME = 'Strict-Transport-Security'.freeze
|
|
6
6
|
HSTS_MAX_AGE = "631138519"
|
|
7
7
|
DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE
|
|
8
8
|
VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i
|
|
@@ -2,7 +2,7 @@ module SecureHeaders
|
|
|
2
2
|
class XContentTypeOptionsConfigError < StandardError; end
|
|
3
3
|
|
|
4
4
|
class XContentTypeOptions
|
|
5
|
-
HEADER_NAME = "X-Content-Type-Options"
|
|
5
|
+
HEADER_NAME = "X-Content-Type-Options".freeze
|
|
6
6
|
DEFAULT_VALUE = "nosniff"
|
|
7
7
|
CONFIG_KEY = :x_content_type_options
|
|
8
8
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module SecureHeaders
|
|
2
2
|
class XPCDPConfigError < StandardError; end
|
|
3
3
|
class XPermittedCrossDomainPolicies
|
|
4
|
-
HEADER_NAME = "X-Permitted-Cross-Domain-Policies"
|
|
4
|
+
HEADER_NAME = "X-Permitted-Cross-Domain-Policies".freeze
|
|
5
5
|
DEFAULT_VALUE = 'none'
|
|
6
6
|
VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename)
|
|
7
7
|
CONFIG_KEY = :x_permitted_cross_domain_policies
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module SecureHeaders
|
|
2
2
|
class XXssProtectionConfigError < StandardError; end
|
|
3
3
|
class XXssProtection
|
|
4
|
-
HEADER_NAME = 'X-XSS-Protection'
|
|
4
|
+
HEADER_NAME = 'X-XSS-Protection'.freeze
|
|
5
5
|
DEFAULT_VALUE = "1; mode=block"
|
|
6
6
|
VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i
|
|
7
7
|
CONFIG_KEY = :x_xss_protection
|