secure_headers 5.0.5 → 6.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/.travis.yml +8 -4
- data/CHANGELOG.md +5 -1
- data/README.md +2 -2
- data/docs/upgrading-to-6-0.md +50 -0
- data/lib/secure_headers/configuration.rb +114 -164
- data/lib/secure_headers/headers/clear_site_data.rb +1 -3
- data/lib/secure_headers/headers/content_security_policy.rb +6 -66
- data/lib/secure_headers/headers/content_security_policy_config.rb +3 -13
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +2 -3
- data/lib/secure_headers/headers/policy_management.rb +14 -65
- data/lib/secure_headers/headers/public_key_pins.rb +2 -3
- data/lib/secure_headers/headers/referrer_policy.rb +2 -2
- data/lib/secure_headers/headers/strict_transport_security.rb +2 -2
- data/lib/secure_headers/headers/x_content_type_options.rb +2 -2
- data/lib/secure_headers/headers/x_download_options.rb +2 -2
- data/lib/secure_headers/headers/x_frame_options.rb +1 -2
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +2 -2
- data/lib/secure_headers/headers/x_xss_protection.rb +3 -3
- data/lib/secure_headers/view_helper.rb +9 -8
- data/lib/secure_headers.rb +14 -78
- data/secure_headers.gemspec +1 -2
- data/spec/lib/secure_headers/configuration_spec.rb +15 -70
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +2 -65
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +35 -9
- data/spec/lib/secure_headers/middleware_spec.rb +7 -1
- data/spec/lib/secure_headers/view_helpers_spec.rb +29 -0
- data/spec/lib/secure_headers_spec.rb +38 -76
- data/spec/spec_helper.rb +7 -3
- metadata +3 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d7392744d486b32bda2b91432d39ddfeee87dfd
|
|
4
|
+
data.tar.gz: 38328982bf71376b9412ed8c7d0652163741bf16
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e08d9df89db6908d0c8419d0086bf8fb6c7e374c53e0bf774bd74d534094c66ef3cdcbac23e97c03b05d4045ba6878c3f6d8a17877146bdb6f2f0c15b57d1784
|
|
7
|
+
data.tar.gz: 304824374c236d11edc52049732a2ea133bbbdf611f35b2c5309a950125e879fb4016f0f15d7ad6c09310ab4b6080e966fac912147f43ed91989aa15898595a7
|
data/.travis.yml
CHANGED
|
@@ -2,9 +2,10 @@ language: ruby
|
|
|
2
2
|
|
|
3
3
|
rvm:
|
|
4
4
|
- ruby-head
|
|
5
|
-
- 2.
|
|
6
|
-
- 2.3
|
|
7
|
-
- 2.
|
|
5
|
+
- 2.5.0
|
|
6
|
+
- 2.4.3
|
|
7
|
+
- 2.3.6
|
|
8
|
+
- 2.2.9
|
|
8
9
|
- jruby-head
|
|
9
10
|
|
|
10
11
|
env:
|
|
@@ -18,7 +19,10 @@ matrix:
|
|
|
18
19
|
- rvm: jruby-head
|
|
19
20
|
- rvm: ruby-head
|
|
20
21
|
|
|
21
|
-
before_install:
|
|
22
|
+
before_install:
|
|
23
|
+
- gem update --system
|
|
24
|
+
- gem --version
|
|
25
|
+
- gem update bundler
|
|
22
26
|
bundler_args: --without guard -j 3
|
|
23
27
|
|
|
24
28
|
sudo: false
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
## 6.0
|
|
2
|
+
|
|
3
|
+
- See the [upgrading to 6.0](docs/upgrading-to-6-0.md) guide for the breaking changes.
|
|
4
|
+
|
|
1
5
|
## 5.0.5
|
|
2
6
|
|
|
3
|
-
A release to deprecate `SecureHeaders::Configuration#get` in prep for 6.x
|
|
7
|
+
- A release to deprecate `SecureHeaders::Configuration#get` in prep for 6.x
|
|
4
8
|
|
|
5
9
|
## 5.0.4
|
|
6
10
|
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Secure Headers [](http://travis-ci.org/twitter/secureheaders) [](https://codeclimate.com/github/twitter/secureheaders) [](https://coveralls.io/r/twitter/secureheaders)
|
|
2
2
|
|
|
3
|
-
**master represents
|
|
3
|
+
**master represents 6.x line**. See the [upgrading to 4.x doc](docs/upgrading-to-4-0.md), [upgrading to 5.x doc](docs/upgrading-to-5-0.md), or [upgrading to 6.x doc](docs/upgrading-to-6-0.md) for instructions on how to upgrade. Bug fixes should go in the 5.x branch for now.
|
|
4
4
|
|
|
5
5
|
**The [3.x](https://github.com/twitter/secureheaders/tree/2.x) branch is moving into maintenance mode**. See the [upgrading to 3.x doc](docs/upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch.
|
|
6
6
|
|
|
@@ -77,7 +77,7 @@ SecureHeaders::Configuration.default do |config|
|
|
|
77
77
|
preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
|
|
78
78
|
|
|
79
79
|
# directive values: these values will directly translate into source directives
|
|
80
|
-
default_src: %w(
|
|
80
|
+
default_src: %w('none'),
|
|
81
81
|
base_uri: %w('self'),
|
|
82
82
|
block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/
|
|
83
83
|
child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
## Named overrides are now dynamically applied
|
|
2
|
+
|
|
3
|
+
The original implementation of name overrides worked by making a copy of the default policy, applying the overrides, and storing the result for later use. But, this lead to unexpected results if named overrides were combined with a dynamic policy change. If a change was made to the default configuration during a request, followed by a named override, the dynamic changes would be lost. To keep things consistent named overrides have been rewritten to work the same as named appends in that they always operate on the configuration for the current request. As an example:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
class ApplicationController < ActionController::Base
|
|
7
|
+
Configuration.default do |config|
|
|
8
|
+
config.x_frame_options = OPT_OUT
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
SecureHeaders::Configuration.override(:dynamic_override) do |config|
|
|
12
|
+
config.x_content_type_options = "nosniff"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class FooController < ApplicationController
|
|
17
|
+
def bar
|
|
18
|
+
# Dynamically update the default config for this request
|
|
19
|
+
override_x_frame_options("DENY")
|
|
20
|
+
append_content_security_policy_directives(frame_src: "3rdpartyprovider.com")
|
|
21
|
+
|
|
22
|
+
# Override everything, discard modifications above
|
|
23
|
+
use_secure_headers_override(:dynamic_override)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Prior to 6.0.0, the response would NOT include a `X-Frame-Options` header since the named override would be a copy of the default configuration, but with `X-Content-Type-Options` set to `nosniff`. As of 6.0.0, the above code results in both `X-Frame-Options` set to `DENY` AND `X-Content-Type-Options` set to `nosniff`.
|
|
29
|
+
|
|
30
|
+
## `ContentSecurityPolicyConfig#merge` and `ContentSecurityPolicyReportOnlyConfig#merge` work more like `Hash#merge`
|
|
31
|
+
|
|
32
|
+
These classes are typically not directly instantiated by users of SecureHeaders. But, if you access `config.csp` you end up accessing one of these objects. Prior to 6.0.0, `#merge` worked more like `#append` in that it would combine policies (i.e. if both policies contained the same key the values would be combined rather than overwritten). This was not consistent with `#merge!`, which worked more like ruby's `Hash#merge!` (overwriting duplicate keys). As of 6.0.0, `#merge` works the same as `#merge!`, but returns a new object instead of mutating `self`.
|
|
33
|
+
|
|
34
|
+
## `Configuration#get` has been removed
|
|
35
|
+
|
|
36
|
+
This method is not typically directly called by users of SecureHeaders. Given that named overrides are no longer statically stored, fetching them no longer makes sense.
|
|
37
|
+
|
|
38
|
+
## Configuration headers are no longer cached
|
|
39
|
+
|
|
40
|
+
Prior to 6.0.0 SecureHeaders pre-built and cached the headers that corresponded to the default configuration. The same was also done for named overrides. However, now that named overrides are applied dynamically, those can no longer be cached. As a result, caching has been removed in the name of simplicity. Some micro-benchmarks indicate this shouldn't be a performance problem and will help to eliminate a class of bugs entirely.
|
|
41
|
+
|
|
42
|
+
## Configuration the default configuration more than once will result in an Exception
|
|
43
|
+
|
|
44
|
+
Prior to 6.0.0 you could conceivably, though unlikely, have `Configure#default` called more than once. Because configurations are dynamic, configuring more than once could result in unexpected behavior. So, as of 6.0.0 we raise `AlreadyConfiguredError` if the default configuration is setup more than once.
|
|
45
|
+
|
|
46
|
+
## All user agent sniffing has been removed
|
|
47
|
+
|
|
48
|
+
The policy configured is the policy that is delivered in terms of which directives are sent. We still dedup, strip schemes, and look for other optimizations but we will not e.g. conditionally send `frame-src` / `child-src` or apply `nonce`s / `unsafe-inline`.
|
|
49
|
+
|
|
50
|
+
The primary reason for these per-browser customization was to reduce console warnings. This has lead to many bugs and results inc confusing behavior. Also, console logs are incredibly noisy today and increasingly warn you about perfectly valid things (like sending `X-Frame-Options` and `frame-ancestors` together).
|
|
@@ -4,7 +4,8 @@ require "yaml"
|
|
|
4
4
|
module SecureHeaders
|
|
5
5
|
class Configuration
|
|
6
6
|
DEFAULT_CONFIG = :default
|
|
7
|
-
|
|
7
|
+
NOOP_OVERRIDE = "secure_headers_noop_override"
|
|
8
|
+
class AlreadyConfiguredError < StandardError; end
|
|
8
9
|
class NotYetConfiguredError < StandardError; end
|
|
9
10
|
class IllegalPolicyModificationError < StandardError; end
|
|
10
11
|
class << self
|
|
@@ -14,9 +15,21 @@ module SecureHeaders
|
|
|
14
15
|
#
|
|
15
16
|
# Returns the newly created config.
|
|
16
17
|
def default(&block)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
if defined?(@default_config)
|
|
19
|
+
raise AlreadyConfiguredError, "Policy already configured"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Define a built-in override that clears all configuration options and
|
|
23
|
+
# results in no security headers being set.
|
|
24
|
+
override(NOOP_OVERRIDE) do |config|
|
|
25
|
+
CONFIG_ATTRIBUTES.each do |attr|
|
|
26
|
+
config.instance_variable_set("@#{attr}", OPT_OUT)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
new_config = new(&block).freeze
|
|
31
|
+
new_config.validate_config!
|
|
32
|
+
@default_config = new_config
|
|
20
33
|
end
|
|
21
34
|
alias_method :configure, :default
|
|
22
35
|
|
|
@@ -27,28 +40,15 @@ module SecureHeaders
|
|
|
27
40
|
# if no value is supplied.
|
|
28
41
|
#
|
|
29
42
|
# Returns: the newly created config
|
|
30
|
-
def override(name,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
override = @configurations[base].dup
|
|
35
|
-
override.instance_eval(&block) if block_given?
|
|
36
|
-
add_configuration(name, override)
|
|
43
|
+
def override(name, &block)
|
|
44
|
+
@overrides ||= {}
|
|
45
|
+
raise "Provide a configuration block" unless block_given?
|
|
46
|
+
@overrides[name] = block
|
|
37
47
|
end
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# NotYetConfiguredError if `default` has not been called.
|
|
43
|
-
def get(name = DEFAULT_CONFIG, internal: false)
|
|
44
|
-
unless internal
|
|
45
|
-
Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#get` is deprecated. It will be removed in the next major release. Use SecureHeaders::Configuration.dup to retrieve the default config."
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
if @configurations.nil?
|
|
49
|
-
raise NotYetConfiguredError, "Default policy not yet supplied"
|
|
50
|
-
end
|
|
51
|
-
@configurations[name]
|
|
49
|
+
def overrides(name)
|
|
50
|
+
@overrides ||= {}
|
|
51
|
+
@overrides[name]
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def named_appends(name)
|
|
@@ -56,44 +56,17 @@ module SecureHeaders
|
|
|
56
56
|
@appends[name]
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
def named_append(name,
|
|
59
|
+
def named_append(name, &block)
|
|
60
60
|
@appends ||= {}
|
|
61
61
|
raise "Provide a configuration block" unless block_given?
|
|
62
62
|
@appends[name] = block
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# Private: add a valid configuration to the global set of named configs.
|
|
68
|
-
#
|
|
69
|
-
# config - the config to store
|
|
70
|
-
# name - the lookup value for this config
|
|
71
|
-
#
|
|
72
|
-
# Raises errors if the config is invalid or if a config named `name`
|
|
73
|
-
# already exists.
|
|
74
|
-
#
|
|
75
|
-
# Returns the config, if valid
|
|
76
|
-
def add_configuration(name, config)
|
|
77
|
-
config.validate_config!
|
|
78
|
-
@configurations ||= {}
|
|
79
|
-
config.send(:cache_headers!)
|
|
80
|
-
config.send(:cache_hpkp_report_host)
|
|
81
|
-
config.freeze
|
|
82
|
-
@configurations[name] = config
|
|
65
|
+
def dup
|
|
66
|
+
default_config.dup
|
|
83
67
|
end
|
|
84
68
|
|
|
85
|
-
|
|
86
|
-
#
|
|
87
|
-
# Returns the noop config
|
|
88
|
-
def add_noop_configuration
|
|
89
|
-
noop_config = new do |config|
|
|
90
|
-
ALL_HEADER_CLASSES.each do |klass|
|
|
91
|
-
config.send("#{klass::CONFIG_KEY}=", OPT_OUT)
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
add_configuration(NOOP_CONFIGURATION, noop_config)
|
|
96
|
-
end
|
|
69
|
+
private
|
|
97
70
|
|
|
98
71
|
# Public: perform a basic deep dup. The shallow copy provided by dup/clone
|
|
99
72
|
# can lead to modifying parent objects.
|
|
@@ -108,6 +81,17 @@ module SecureHeaders
|
|
|
108
81
|
end
|
|
109
82
|
end
|
|
110
83
|
|
|
84
|
+
# Private: Returns the internal default configuration. This should only
|
|
85
|
+
# ever be called by internal callers (or tests) that know the semantics
|
|
86
|
+
# of ensuring that the default config is never mutated and is dup(ed)
|
|
87
|
+
# before it is used in a request.
|
|
88
|
+
def default_config
|
|
89
|
+
unless defined?(@default_config)
|
|
90
|
+
raise NotYetConfiguredError, "Default policy not yet configured"
|
|
91
|
+
end
|
|
92
|
+
@default_config
|
|
93
|
+
end
|
|
94
|
+
|
|
111
95
|
# Private: convenience method purely DRY things up. The value may not be a
|
|
112
96
|
# hash (e.g. OPT_OUT, nil)
|
|
113
97
|
def deep_copy_if_hash(value)
|
|
@@ -119,11 +103,31 @@ module SecureHeaders
|
|
|
119
103
|
end
|
|
120
104
|
end
|
|
121
105
|
|
|
122
|
-
|
|
123
|
-
:
|
|
124
|
-
:
|
|
106
|
+
CONFIG_ATTRIBUTES_TO_HEADER_CLASSES = {
|
|
107
|
+
hsts: StrictTransportSecurity,
|
|
108
|
+
x_frame_options: XFrameOptions,
|
|
109
|
+
x_content_type_options: XContentTypeOptions,
|
|
110
|
+
x_xss_protection: XXssProtection,
|
|
111
|
+
x_download_options: XDownloadOptions,
|
|
112
|
+
x_permitted_cross_domain_policies: XPermittedCrossDomainPolicies,
|
|
113
|
+
referrer_policy: ReferrerPolicy,
|
|
114
|
+
clear_site_data: ClearSiteData,
|
|
115
|
+
expect_certificate_transparency: ExpectCertificateTransparency,
|
|
116
|
+
csp: ContentSecurityPolicy,
|
|
117
|
+
csp_report_only: ContentSecurityPolicy,
|
|
118
|
+
hpkp: PublicKeyPins,
|
|
119
|
+
cookies: Cookie,
|
|
120
|
+
}.freeze
|
|
121
|
+
|
|
122
|
+
CONFIG_ATTRIBUTES = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.keys.freeze
|
|
123
|
+
|
|
124
|
+
# The list of attributes that must respond to a `validate_config!` method
|
|
125
|
+
VALIDATABLE_ATTRIBUTES = CONFIG_ATTRIBUTES
|
|
125
126
|
|
|
126
|
-
|
|
127
|
+
# The list of attributes that must respond to a `make_header` method
|
|
128
|
+
HEADERABLE_ATTRIBUTES = (CONFIG_ATTRIBUTES - [:cookies]).freeze
|
|
129
|
+
|
|
130
|
+
attr_accessor(*CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.keys)
|
|
127
131
|
|
|
128
132
|
@script_hashes = nil
|
|
129
133
|
@style_hashes = nil
|
|
@@ -140,7 +144,6 @@ module SecureHeaders
|
|
|
140
144
|
@clear_site_data = nil
|
|
141
145
|
@csp = nil
|
|
142
146
|
@csp_report_only = nil
|
|
143
|
-
@hpkp_report_host = nil
|
|
144
147
|
@hpkp = nil
|
|
145
148
|
@hsts = nil
|
|
146
149
|
@x_content_type_options = nil
|
|
@@ -158,7 +161,7 @@ module SecureHeaders
|
|
|
158
161
|
instance_eval(&block) if block_given?
|
|
159
162
|
end
|
|
160
163
|
|
|
161
|
-
# Public: copy everything
|
|
164
|
+
# Public: copy everything
|
|
162
165
|
#
|
|
163
166
|
# Returns a deep-dup'd copy of this configuration.
|
|
164
167
|
def dup
|
|
@@ -166,7 +169,6 @@ module SecureHeaders
|
|
|
166
169
|
copy.cookies = self.class.send(:deep_copy_if_hash, @cookies)
|
|
167
170
|
copy.csp = @csp.dup if @csp
|
|
168
171
|
copy.csp_report_only = @csp_report_only.dup if @csp_report_only
|
|
169
|
-
copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
|
|
170
172
|
copy.x_content_type_options = @x_content_type_options
|
|
171
173
|
copy.hsts = @hsts
|
|
172
174
|
copy.x_frame_options = @x_frame_options
|
|
@@ -177,18 +179,39 @@ module SecureHeaders
|
|
|
177
179
|
copy.expect_certificate_transparency = @expect_certificate_transparency
|
|
178
180
|
copy.referrer_policy = @referrer_policy
|
|
179
181
|
copy.hpkp = @hpkp
|
|
180
|
-
copy.hpkp_report_host = @hpkp_report_host
|
|
181
182
|
copy
|
|
182
183
|
end
|
|
183
184
|
|
|
185
|
+
# Public: Apply a named override to the current config
|
|
186
|
+
#
|
|
187
|
+
# Returns self
|
|
188
|
+
def override(name = nil, &block)
|
|
189
|
+
if override = self.class.overrides(name)
|
|
190
|
+
instance_eval(&override)
|
|
191
|
+
else
|
|
192
|
+
raise ArgumentError.new("no override by the name of #{name} has been configured")
|
|
193
|
+
end
|
|
194
|
+
self
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def generate_headers
|
|
198
|
+
headers = {}
|
|
199
|
+
HEADERABLE_ATTRIBUTES.each do |attr|
|
|
200
|
+
klass = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr]
|
|
201
|
+
header_name, value = klass.make_header(instance_variable_get("@#{attr}"))
|
|
202
|
+
if header_name && value
|
|
203
|
+
headers[header_name] = value
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
headers
|
|
207
|
+
end
|
|
208
|
+
|
|
184
209
|
def opt_out(header)
|
|
185
210
|
send("#{header}=", OPT_OUT)
|
|
186
|
-
self.cached_headers.delete(header)
|
|
187
211
|
end
|
|
188
212
|
|
|
189
213
|
def update_x_frame_options(value)
|
|
190
214
|
@x_frame_options = value
|
|
191
|
-
self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value)
|
|
192
215
|
end
|
|
193
216
|
|
|
194
217
|
# Public: validates all configurations values.
|
|
@@ -197,19 +220,10 @@ module SecureHeaders
|
|
|
197
220
|
#
|
|
198
221
|
# Returns nothing
|
|
199
222
|
def validate_config!
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
XFrameOptions.validate_config!(@x_frame_options)
|
|
205
|
-
XContentTypeOptions.validate_config!(@x_content_type_options)
|
|
206
|
-
XXssProtection.validate_config!(@x_xss_protection)
|
|
207
|
-
XDownloadOptions.validate_config!(@x_download_options)
|
|
208
|
-
XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
|
|
209
|
-
ClearSiteData.validate_config!(@clear_site_data)
|
|
210
|
-
ExpectCertificateTransparency.validate_config!(@expect_certificate_transparency)
|
|
211
|
-
PublicKeyPins.validate_config!(@hpkp)
|
|
212
|
-
Cookie.validate_config!(@cookies)
|
|
223
|
+
VALIDATABLE_ATTRIBUTES.each do |attr|
|
|
224
|
+
klass = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr]
|
|
225
|
+
klass.validate_config!(instance_variable_get("@#{attr}"))
|
|
226
|
+
end
|
|
213
227
|
end
|
|
214
228
|
|
|
215
229
|
def secure_cookies=(secure_cookies)
|
|
@@ -217,15 +231,15 @@ module SecureHeaders
|
|
|
217
231
|
end
|
|
218
232
|
|
|
219
233
|
def csp=(new_csp)
|
|
220
|
-
|
|
221
|
-
|
|
234
|
+
case new_csp
|
|
235
|
+
when OPT_OUT
|
|
236
|
+
@csp = new_csp
|
|
237
|
+
when ContentSecurityPolicyConfig
|
|
238
|
+
@csp = new_csp
|
|
239
|
+
when Hash
|
|
240
|
+
@csp = ContentSecurityPolicyConfig.new(new_csp)
|
|
222
241
|
else
|
|
223
|
-
|
|
224
|
-
# invalid configuration implies that CSPRO should be set, CSP should not - so opt out
|
|
225
|
-
raise ArgumentError, "#{Kernel.caller.first}: `#csp=` was supplied a config with report_only: true. Use #csp_report_only="
|
|
226
|
-
else
|
|
227
|
-
@csp = ContentSecurityPolicyConfig.new(new_csp)
|
|
228
|
-
end
|
|
242
|
+
raise ArgumentError, "Must provide either an existing CSP config or a CSP config hash"
|
|
229
243
|
end
|
|
230
244
|
end
|
|
231
245
|
|
|
@@ -236,87 +250,23 @@ module SecureHeaders
|
|
|
236
250
|
# configuring csp_report_only, the code will assume you mean to only use
|
|
237
251
|
# report-only mode and you will be opted-out of enforce mode.
|
|
238
252
|
def csp_report_only=(new_csp)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
end
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
protected
|
|
255
|
-
|
|
256
|
-
def cookies=(cookies)
|
|
257
|
-
@cookies = cookies
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def cached_headers=(headers)
|
|
261
|
-
@cached_headers = headers
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def hpkp=(hpkp)
|
|
265
|
-
@hpkp = self.class.send(:deep_copy_if_hash, hpkp)
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
def hpkp_report_host=(hpkp_report_host)
|
|
269
|
-
@hpkp_report_host = hpkp_report_host
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
private
|
|
273
|
-
|
|
274
|
-
def cache_hpkp_report_host
|
|
275
|
-
has_report_uri = @hpkp && @hpkp != OPT_OUT && @hpkp[:report_uri]
|
|
276
|
-
self.hpkp_report_host = if has_report_uri
|
|
277
|
-
parsed_report_uri = URI.parse(@hpkp[:report_uri])
|
|
278
|
-
parsed_report_uri.host
|
|
279
|
-
end
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
# Public: Precompute the header names and values for this configuration.
|
|
283
|
-
# Ensures that headers generated at configure time, not on demand.
|
|
284
|
-
#
|
|
285
|
-
# Returns the cached headers
|
|
286
|
-
def cache_headers!
|
|
287
|
-
# generate defaults for the "easy" headers
|
|
288
|
-
headers = (ALL_HEADERS_BESIDES_CSP).each_with_object({}) do |klass, hash|
|
|
289
|
-
config = instance_variable_get("@#{klass::CONFIG_KEY}")
|
|
290
|
-
unless config == OPT_OUT
|
|
291
|
-
hash[klass::CONFIG_KEY] = klass.make_header(config).freeze
|
|
292
|
-
end
|
|
253
|
+
case new_csp
|
|
254
|
+
when OPT_OUT
|
|
255
|
+
@csp_report_only = new_csp
|
|
256
|
+
when ContentSecurityPolicyReportOnlyConfig
|
|
257
|
+
@csp_report_only = new_csp.dup
|
|
258
|
+
when ContentSecurityPolicyConfig
|
|
259
|
+
@csp_report_only = new_csp.make_report_only
|
|
260
|
+
when Hash
|
|
261
|
+
@csp_report_only = ContentSecurityPolicyReportOnlyConfig.new(new_csp)
|
|
262
|
+
else
|
|
263
|
+
raise ArgumentError, "Must provide either an existing CSP config or a CSP config hash"
|
|
293
264
|
end
|
|
294
|
-
|
|
295
|
-
generate_csp_headers(headers)
|
|
296
|
-
|
|
297
|
-
headers.freeze
|
|
298
|
-
self.cached_headers = headers
|
|
299
265
|
end
|
|
300
266
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
# different variations
|
|
305
|
-
#
|
|
306
|
-
# Returns nothing
|
|
307
|
-
def generate_csp_headers(headers)
|
|
308
|
-
generate_csp_headers_for_config(headers, ContentSecurityPolicyConfig::CONFIG_KEY, self.csp)
|
|
309
|
-
generate_csp_headers_for_config(headers, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY, self.csp_report_only)
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
def generate_csp_headers_for_config(headers, header_key, csp_config)
|
|
313
|
-
unless csp_config.opt_out?
|
|
314
|
-
headers[header_key] = {}
|
|
315
|
-
ContentSecurityPolicy::VARIATIONS.each_key do |name|
|
|
316
|
-
csp = ContentSecurityPolicy.make_header(csp_config, UserAgent.parse(name))
|
|
317
|
-
headers[header_key][name] = csp.freeze
|
|
318
|
-
end
|
|
319
|
-
end
|
|
267
|
+
def hpkp_report_host
|
|
268
|
+
return nil unless @hpkp && hpkp != OPT_OUT && @hpkp[:report_uri]
|
|
269
|
+
URI.parse(@hpkp[:report_uri]).host
|
|
320
270
|
end
|
|
321
271
|
end
|
|
322
272
|
end
|
|
@@ -11,13 +11,11 @@ module SecureHeaders
|
|
|
11
11
|
EXECUTION_CONTEXTS = "executionContexts".freeze
|
|
12
12
|
ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECUTION_CONTEXTS]
|
|
13
13
|
|
|
14
|
-
CONFIG_KEY = :clear_site_data
|
|
15
|
-
|
|
16
14
|
class << self
|
|
17
15
|
# Public: make an Clear-Site-Data header name, value pair
|
|
18
16
|
#
|
|
19
17
|
# Returns nil if not configured, returns header name and value if configured.
|
|
20
|
-
def make_header(config = nil)
|
|
18
|
+
def make_header(config = nil, user_agent = nil)
|
|
21
19
|
case config
|
|
22
20
|
when nil, OPT_OUT, []
|
|
23
21
|
# noop
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require_relative "policy_management"
|
|
3
3
|
require_relative "content_security_policy_config"
|
|
4
|
-
require "useragent"
|
|
5
4
|
|
|
6
5
|
module SecureHeaders
|
|
7
6
|
class ContentSecurityPolicy
|
|
8
7
|
include PolicyManagement
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
VERSION_46 = ::UserAgent::Version.new("46")
|
|
12
|
-
VERSION_10 = ::UserAgent::Version.new("10")
|
|
13
|
-
FALLBACK_VERSION = ::UserAgent::Version.new("0")
|
|
14
|
-
|
|
15
|
-
def initialize(config = nil, user_agent = OTHER)
|
|
9
|
+
def initialize(config = nil)
|
|
16
10
|
@config = if config.is_a?(Hash)
|
|
17
11
|
if config[:report_only]
|
|
18
12
|
ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
|
|
@@ -25,12 +19,6 @@ module SecureHeaders
|
|
|
25
19
|
config
|
|
26
20
|
end
|
|
27
21
|
|
|
28
|
-
@parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
|
|
29
|
-
user_agent
|
|
30
|
-
else
|
|
31
|
-
UserAgent.parse(user_agent)
|
|
32
|
-
end
|
|
33
|
-
@frame_src = normalize_child_frame_src
|
|
34
22
|
@preserve_schemes = @config.preserve_schemes
|
|
35
23
|
@script_nonce = @config.script_nonce
|
|
36
24
|
@style_nonce = @config.style_nonce
|
|
@@ -55,20 +43,10 @@ module SecureHeaders
|
|
|
55
43
|
|
|
56
44
|
private
|
|
57
45
|
|
|
58
|
-
def normalize_child_frame_src
|
|
59
|
-
if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src
|
|
60
|
-
raise ArgumentError, "#{Kernel.caller.first}: both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers."
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
@config.frame_src || @config.child_src
|
|
64
|
-
end
|
|
65
|
-
|
|
66
46
|
# Private: converts the config object into a string representing a policy.
|
|
67
47
|
# Places default-src at the first directive and report-uri as the last. All
|
|
68
48
|
# others are presented in alphabetical order.
|
|
69
49
|
#
|
|
70
|
-
# Unsupported directives are filtered based on the user agent.
|
|
71
|
-
#
|
|
72
50
|
# Returns a content security policy header value.
|
|
73
51
|
def build_value
|
|
74
52
|
directives.map do |directive_name|
|
|
@@ -124,18 +102,7 @@ module SecureHeaders
|
|
|
124
102
|
#
|
|
125
103
|
# Returns a string representing a directive.
|
|
126
104
|
def build_source_list_directive(directive)
|
|
127
|
-
source_list =
|
|
128
|
-
when :child_src
|
|
129
|
-
if supported_directives.include?(:child_src)
|
|
130
|
-
@frame_src
|
|
131
|
-
end
|
|
132
|
-
when :frame_src
|
|
133
|
-
unless supported_directives.include?(:child_src)
|
|
134
|
-
@frame_src
|
|
135
|
-
end
|
|
136
|
-
else
|
|
137
|
-
@config.directive_value(directive)
|
|
138
|
-
end
|
|
105
|
+
source_list = @config.directive_value(directive)
|
|
139
106
|
|
|
140
107
|
if source_list != OPT_OUT && source_list && source_list.any?
|
|
141
108
|
normalized_source_list = minify_source_list(directive, source_list)
|
|
@@ -212,23 +179,19 @@ module SecureHeaders
|
|
|
212
179
|
# unsafe-inline, this is more concise.
|
|
213
180
|
def append_nonce(source_list, nonce)
|
|
214
181
|
if nonce
|
|
215
|
-
|
|
216
|
-
source_list << "'nonce-#{nonce}'"
|
|
217
|
-
else
|
|
218
|
-
source_list << UNSAFE_INLINE
|
|
219
|
-
end
|
|
182
|
+
source_list.push("'nonce-#{nonce}'", UNSAFE_INLINE)
|
|
220
183
|
end
|
|
221
184
|
|
|
222
185
|
source_list
|
|
223
186
|
end
|
|
224
187
|
|
|
225
|
-
# Private: return the list of directives
|
|
188
|
+
# Private: return the list of directives,
|
|
226
189
|
# starting with default-src and ending with report-uri.
|
|
227
190
|
def directives
|
|
228
191
|
[
|
|
229
192
|
DEFAULT_SRC,
|
|
230
|
-
BODY_DIRECTIVES
|
|
231
|
-
REPORT_URI
|
|
193
|
+
BODY_DIRECTIVES,
|
|
194
|
+
REPORT_URI,
|
|
232
195
|
].flatten
|
|
233
196
|
end
|
|
234
197
|
|
|
@@ -237,29 +200,6 @@ module SecureHeaders
|
|
|
237
200
|
source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
|
|
238
201
|
end
|
|
239
202
|
|
|
240
|
-
# Private: determine which directives are supported for the given user agent.
|
|
241
|
-
#
|
|
242
|
-
# Add UA-sniffing special casing here.
|
|
243
|
-
#
|
|
244
|
-
# Returns an array of symbols representing the directives.
|
|
245
|
-
def supported_directives
|
|
246
|
-
@supported_directives ||= if VARIATIONS[@parsed_ua.browser]
|
|
247
|
-
if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
|
|
248
|
-
VARIATIONS["FirefoxTransitional"]
|
|
249
|
-
elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10)
|
|
250
|
-
VARIATIONS["SafariTransitional"]
|
|
251
|
-
else
|
|
252
|
-
VARIATIONS[@parsed_ua.browser]
|
|
253
|
-
end
|
|
254
|
-
else
|
|
255
|
-
VARIATIONS[OTHER]
|
|
256
|
-
end
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
def nonces_supported?
|
|
260
|
-
@nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
|
|
261
|
-
end
|
|
262
|
-
|
|
263
203
|
def symbol_to_hyphen_case(sym)
|
|
264
204
|
sym.to_s.tr("_", "-")
|
|
265
205
|
end
|