secure_headers 3.5.0.pre → 3.6.2
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/.travis.yml +2 -0
- data/CHANGELOG.md +20 -1
- data/Gemfile +1 -0
- data/LICENSE +4 -199
- data/README.md +37 -388
- 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 +5 -3
- data/lib/secure_headers/headers/clear_site_data.rb +51 -0
- data/lib/secure_headers/headers/content_security_policy.rb +6 -2
- data/lib/secure_headers/headers/content_security_policy_config.rb +21 -12
- data/lib/secure_headers/headers/policy_management.rb +14 -2
- data/lib/secure_headers/headers/referrer_policy.rb +4 -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 +8 -0
- data/lib/secure_headers.rb +4 -1
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +103 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +17 -0
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +18 -0
- data/spec/lib/secure_headers/middleware_spec.rb +12 -0
- data/spec/lib/secure_headers/view_helpers_spec.rb +10 -0
- data/spec/lib/secure_headers_spec.rb +22 -0
- data/spec/spec_helper.rb +2 -1
- metadata +14 -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
|
+
```
|
|
@@ -116,7 +116,7 @@ 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
|
|
|
@@ -140,7 +140,7 @@ module SecureHeaders
|
|
|
140
140
|
# Returns a deep-dup'd copy of this configuration.
|
|
141
141
|
def dup
|
|
142
142
|
copy = self.class.new
|
|
143
|
-
copy.cookies = @cookies
|
|
143
|
+
copy.cookies = self.class.send(:deep_copy_if_hash, @cookies)
|
|
144
144
|
copy.csp = @csp.dup if @csp
|
|
145
145
|
copy.csp_report_only = @csp_report_only.dup if @csp_report_only
|
|
146
146
|
copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
|
|
@@ -150,6 +150,7 @@ module SecureHeaders
|
|
|
150
150
|
copy.x_xss_protection = @x_xss_protection
|
|
151
151
|
copy.x_download_options = @x_download_options
|
|
152
152
|
copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
|
|
153
|
+
copy.clear_site_data = @clear_site_data
|
|
153
154
|
copy.referrer_policy = @referrer_policy
|
|
154
155
|
copy.hpkp = @hpkp
|
|
155
156
|
copy.hpkp_report_host = @hpkp_report_host
|
|
@@ -181,6 +182,7 @@ module SecureHeaders
|
|
|
181
182
|
XXssProtection.validate_config!(@x_xss_protection)
|
|
182
183
|
XDownloadOptions.validate_config!(@x_download_options)
|
|
183
184
|
XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
|
|
185
|
+
ClearSiteData.validate_config!(@clear_site_data)
|
|
184
186
|
PublicKeyPins.validate_config!(@hpkp)
|
|
185
187
|
Cookie.validate_config!(@cookies)
|
|
186
188
|
end
|
|
@@ -260,7 +262,7 @@ module SecureHeaders
|
|
|
260
262
|
end
|
|
261
263
|
end
|
|
262
264
|
|
|
263
|
-
# Public: Precompute the header names and values for this
|
|
265
|
+
# Public: Precompute the header names and values for this configuration.
|
|
264
266
|
# Ensures that headers generated at configure time, not on demand.
|
|
265
267
|
#
|
|
266
268
|
# Returns the cached headers
|
|
@@ -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
|
|
@@ -8,6 +8,8 @@ module SecureHeaders
|
|
|
8
8
|
|
|
9
9
|
# constants to be used for version-specific UA sniffing
|
|
10
10
|
VERSION_46 = ::UserAgent::Version.new("46")
|
|
11
|
+
VERSION_10 = ::UserAgent::Version.new("10")
|
|
12
|
+
FALLBACK_VERSION = ::UserAgent::Version.new("0")
|
|
11
13
|
|
|
12
14
|
def initialize(config = nil, user_agent = OTHER)
|
|
13
15
|
@config = if config.is_a?(Hash)
|
|
@@ -212,8 +214,10 @@ module SecureHeaders
|
|
|
212
214
|
# Returns an array of symbols representing the directives.
|
|
213
215
|
def supported_directives
|
|
214
216
|
@supported_directives ||= if VARIATIONS[@parsed_ua.browser]
|
|
215
|
-
if @parsed_ua.browser == "Firefox" && @parsed_ua.version >= VERSION_46
|
|
217
|
+
if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
|
|
216
218
|
VARIATIONS["FirefoxTransitional"]
|
|
219
|
+
elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10)
|
|
220
|
+
VARIATIONS["SafariTransitional"]
|
|
217
221
|
else
|
|
218
222
|
VARIATIONS[@parsed_ua.browser]
|
|
219
223
|
end
|
|
@@ -223,7 +227,7 @@ module SecureHeaders
|
|
|
223
227
|
end
|
|
224
228
|
|
|
225
229
|
def nonces_supported?
|
|
226
|
-
@nonces_supported ||=
|
|
230
|
+
@nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
|
|
227
231
|
end
|
|
228
232
|
|
|
229
233
|
def symbol_to_hyphen_case(sym)
|
|
@@ -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
|
|
@@ -52,8 +47,9 @@ module SecureHeaders
|
|
|
52
47
|
|
|
53
48
|
def to_h
|
|
54
49
|
self.class.attrs.each_with_object({}) do |key, hash|
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
value = self.send(key)
|
|
51
|
+
hash[key] = value unless value.nil?
|
|
52
|
+
end
|
|
57
53
|
end
|
|
58
54
|
|
|
59
55
|
def dup
|
|
@@ -73,14 +69,26 @@ module SecureHeaders
|
|
|
73
69
|
|
|
74
70
|
private
|
|
75
71
|
def from_hash(hash)
|
|
76
|
-
hash.
|
|
72
|
+
hash.each_pair do |k, v|
|
|
73
|
+
next if v.nil?
|
|
74
|
+
|
|
77
75
|
if self.class.attrs.include?(k)
|
|
78
|
-
|
|
76
|
+
write_attribute(k, v)
|
|
79
77
|
else
|
|
80
|
-
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{
|
|
78
|
+
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{v}"
|
|
81
79
|
end
|
|
82
80
|
end
|
|
83
81
|
end
|
|
82
|
+
|
|
83
|
+
def write_attribute(attr, value)
|
|
84
|
+
value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
|
|
85
|
+
attr_variable = "@#{attr}"
|
|
86
|
+
prev_value = self.instance_variable_get(attr_variable)
|
|
87
|
+
self.instance_variable_set(attr_variable, value)
|
|
88
|
+
if prev_value != value
|
|
89
|
+
@modified = true
|
|
90
|
+
end
|
|
91
|
+
end
|
|
84
92
|
end
|
|
85
93
|
|
|
86
94
|
class ContentSecurityPolicyConfigError < StandardError; end
|
|
@@ -88,8 +96,9 @@ module SecureHeaders
|
|
|
88
96
|
CONFIG_KEY = :csp
|
|
89
97
|
HEADER_NAME = "Content-Security-Policy".freeze
|
|
90
98
|
|
|
99
|
+
ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
|
|
91
100
|
def self.attrs
|
|
92
|
-
|
|
101
|
+
ATTRS
|
|
93
102
|
end
|
|
94
103
|
|
|
95
104
|
include DynamicConfig
|
|
@@ -14,6 +14,7 @@ module SecureHeaders
|
|
|
14
14
|
STAR = "*".freeze
|
|
15
15
|
UNSAFE_INLINE = "'unsafe-inline'".freeze
|
|
16
16
|
UNSAFE_EVAL = "'unsafe-eval'".freeze
|
|
17
|
+
STRICT_DYNAMIC = "'strict-dynamic'".freeze
|
|
17
18
|
|
|
18
19
|
# leftover deprecated values that will be in common use upon upgrading.
|
|
19
20
|
DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze
|
|
@@ -80,6 +81,7 @@ module SecureHeaders
|
|
|
80
81
|
|
|
81
82
|
EDGE_DIRECTIVES = DIRECTIVES_1_0
|
|
82
83
|
SAFARI_DIRECTIVES = DIRECTIVES_1_0
|
|
84
|
+
SAFARI_10_DIRECTIVES = DIRECTIVES_2_0
|
|
83
85
|
|
|
84
86
|
FIREFOX_UNSUPPORTED_DIRECTIVES = [
|
|
85
87
|
BLOCK_ALL_MIXED_CONTENT,
|
|
@@ -108,7 +110,7 @@ module SecureHeaders
|
|
|
108
110
|
DIRECTIVES_2_0 + DIRECTIVES_DRAFT
|
|
109
111
|
).freeze
|
|
110
112
|
|
|
111
|
-
ALL_DIRECTIVES =
|
|
113
|
+
ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT).uniq.sort
|
|
112
114
|
|
|
113
115
|
# Think of default-src and report-uri as the beginning and end respectively,
|
|
114
116
|
# everything else is in between.
|
|
@@ -132,6 +134,7 @@ module SecureHeaders
|
|
|
132
134
|
"Firefox" => FIREFOX_DIRECTIVES,
|
|
133
135
|
"FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
|
|
134
136
|
"Safari" => SAFARI_DIRECTIVES,
|
|
137
|
+
"SafariTransitional" => SAFARI_10_DIRECTIVES,
|
|
135
138
|
"Edge" => EDGE_DIRECTIVES,
|
|
136
139
|
"Other" => CHROME_DIRECTIVES
|
|
137
140
|
}.freeze
|
|
@@ -211,6 +214,15 @@ module SecureHeaders
|
|
|
211
214
|
end
|
|
212
215
|
end
|
|
213
216
|
|
|
217
|
+
# Public: check if a user agent supports CSP nonces
|
|
218
|
+
#
|
|
219
|
+
# user_agent - a String or a UserAgent object
|
|
220
|
+
def nonces_supported?(user_agent)
|
|
221
|
+
user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String)
|
|
222
|
+
MODERN_BROWSERS.include?(user_agent.browser) ||
|
|
223
|
+
user_agent.browser == "Safari" && (user_agent.version || CSP::FALLBACK_VERSION) >= CSP::VERSION_10
|
|
224
|
+
end
|
|
225
|
+
|
|
214
226
|
# Public: combine the values from two different configs.
|
|
215
227
|
#
|
|
216
228
|
# original - the main config
|
|
@@ -261,7 +273,7 @@ module SecureHeaders
|
|
|
261
273
|
# copy the default-src value to the original config. This modifies the original hash.
|
|
262
274
|
def populate_fetch_source_with_default!(original, additions)
|
|
263
275
|
# in case we would be appending to an empty directive, fill it with the default-src value
|
|
264
|
-
additions.
|
|
276
|
+
additions.each_key do |directive|
|
|
265
277
|
if !original[directive] && ((source_list?(directive) && FETCH_SOURCES.include?(directive)) || nonce_added?(original, additions))
|
|
266
278
|
if nonce_added?(original, additions)
|
|
267
279
|
inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
module SecureHeaders
|
|
2
2
|
class ReferrerPolicyConfigError < StandardError; end
|
|
3
3
|
class ReferrerPolicy
|
|
4
|
-
HEADER_NAME = "Referrer-Policy"
|
|
4
|
+
HEADER_NAME = "Referrer-Policy".freeze
|
|
5
5
|
DEFAULT_VALUE = "origin-when-cross-origin"
|
|
6
6
|
VALID_POLICIES = %w(
|
|
7
7
|
no-referrer
|
|
8
8
|
no-referrer-when-downgrade
|
|
9
|
+
same-origin
|
|
10
|
+
strict-origin
|
|
11
|
+
strict-origin-when-cross-origin
|
|
9
12
|
origin
|
|
10
13
|
origin-when-cross-origin
|
|
11
14
|
unsafe-url
|
|
@@ -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
|
|
@@ -34,6 +34,14 @@ module SecureHeaders
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def content_security_policy_script_nonce
|
|
38
|
+
content_security_policy_nonce(:script)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def content_security_policy_style_nonce
|
|
42
|
+
content_security_policy_nonce(:style)
|
|
43
|
+
end
|
|
44
|
+
|
|
37
45
|
##
|
|
38
46
|
# Checks to see if the hashed code is expected and adds the hash source
|
|
39
47
|
# value to the current CSP.
|
data/lib/secure_headers.rb
CHANGED
|
@@ -10,6 +10,7 @@ require "secure_headers/headers/x_content_type_options"
|
|
|
10
10
|
require "secure_headers/headers/x_download_options"
|
|
11
11
|
require "secure_headers/headers/x_permitted_cross_domain_policies"
|
|
12
12
|
require "secure_headers/headers/referrer_policy"
|
|
13
|
+
require "secure_headers/headers/clear_site_data"
|
|
13
14
|
require "secure_headers/middleware"
|
|
14
15
|
require "secure_headers/railtie"
|
|
15
16
|
require "secure_headers/view_helper"
|
|
@@ -47,8 +48,10 @@ module SecureHeaders
|
|
|
47
48
|
SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze
|
|
48
49
|
NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze
|
|
49
50
|
HTTPS = "https".freeze
|
|
51
|
+
CSP = ContentSecurityPolicy
|
|
50
52
|
|
|
51
53
|
ALL_HEADER_CLASSES = [
|
|
54
|
+
ClearSiteData,
|
|
52
55
|
ContentSecurityPolicyConfig,
|
|
53
56
|
ContentSecurityPolicyReportOnlyConfig,
|
|
54
57
|
StrictTransportSecurity,
|
|
@@ -174,7 +177,7 @@ module SecureHeaders
|
|
|
174
177
|
|
|
175
178
|
header_classes_for(request).each_with_object({}) do |klass, hash|
|
|
176
179
|
if header = headers[klass::CONFIG_KEY]
|
|
177
|
-
header_name, value = if
|
|
180
|
+
header_name, value = if klass == ContentSecurityPolicyConfig || klass == ContentSecurityPolicyReportOnlyConfig
|
|
178
181
|
csp_header_for_ua(header, user_agent)
|
|
179
182
|
else
|
|
180
183
|
header
|
data/secure_headers.gemspec
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
2
|
Gem::Specification.new do |gem|
|
|
3
3
|
gem.name = "secure_headers"
|
|
4
|
-
gem.version = "3.
|
|
4
|
+
gem.version = "3.6.2"
|
|
5
5
|
gem.authors = ["Neil Matatall"]
|
|
6
6
|
gem.email = ["neil.matatall@gmail.com"]
|
|
7
7
|
gem.description = 'Security related headers all in one gem.'
|