secure_headers 7.0.0 → 7.2.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/CHANGELOG.md +2 -2
- data/README.md +78 -14
- data/lib/secure_headers/configuration.rb +54 -7
- data/lib/secure_headers/headers/clear_site_data.rb +31 -33
- data/lib/secure_headers/headers/content_security_policy.rb +36 -5
- data/lib/secure_headers/headers/content_security_policy_config.rb +2 -2
- data/lib/secure_headers/headers/cookie.rb +2 -4
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +20 -22
- data/lib/secure_headers/headers/policy_management.rb +27 -9
- data/lib/secure_headers/headers/referrer_policy.rb +20 -22
- data/lib/secure_headers/headers/reporting_endpoints.rb +54 -0
- data/lib/secure_headers/headers/strict_transport_security.rb +13 -15
- data/lib/secure_headers/headers/x_content_type_options.rb +14 -16
- data/lib/secure_headers/headers/x_download_options.rb +14 -16
- data/lib/secure_headers/headers/x_frame_options.rb +14 -16
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +14 -16
- data/lib/secure_headers/headers/x_xss_protection.rb +13 -15
- data/lib/secure_headers/middleware.rb +11 -7
- data/lib/secure_headers/railtie.rb +11 -8
- data/lib/secure_headers/task_helper.rb +65 -0
- data/lib/secure_headers/version.rb +1 -1
- data/lib/secure_headers.rb +26 -2
- data/lib/tasks/tasks.rake +4 -53
- data/secure_headers.gemspec +15 -3
- metadata +31 -67
- data/.github/ISSUE_TEMPLATE.md +0 -41
- data/.github/PULL_REQUEST_TEMPLATE.md +0 -20
- data/.github/dependabot.yml +0 -6
- data/.github/workflows/build.yml +0 -25
- data/.github/workflows/github-release.yml +0 -28
- data/.gitignore +0 -13
- data/.rspec +0 -3
- data/.rubocop.yml +0 -4
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/CODE_OF_CONDUCT.md +0 -46
- data/CONTRIBUTING.md +0 -41
- data/Guardfile +0 -13
- data/Rakefile +0 -32
- data/docs/cookies.md +0 -65
- data/docs/hashes.md +0 -64
- data/docs/named_overrides_and_appends.md +0 -104
- data/docs/per_action_configuration.md +0 -139
- data/docs/sinatra.md +0 -25
- data/docs/upgrading-to-3-0.md +0 -42
- data/docs/upgrading-to-4-0.md +0 -35
- data/docs/upgrading-to-5-0.md +0 -15
- data/docs/upgrading-to-6-0.md +0 -50
- data/docs/upgrading-to-7-0.md +0 -12
- data/spec/lib/secure_headers/configuration_spec.rb +0 -121
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +0 -87
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +0 -215
- data/spec/lib/secure_headers/headers/cookie_spec.rb +0 -179
- data/spec/lib/secure_headers/headers/expect_certificate_transparency_spec.rb +0 -42
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +0 -265
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +0 -91
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +0 -33
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +0 -31
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +0 -29
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +0 -36
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +0 -48
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +0 -47
- data/spec/lib/secure_headers/middleware_spec.rb +0 -117
- data/spec/lib/secure_headers/view_helpers_spec.rb +0 -192
- data/spec/lib/secure_headers_spec.rb +0 -516
- data/spec/spec_helper.rb +0 -64
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3c43b0dda7b2739b7309a887a98fb5a7c81120fd10d3f7eec5b15ad5afb3ef05
|
|
4
|
+
data.tar.gz: d33d9f60cbd50e3085d1b5f0946007af6cd0f56ad179bbf393c2713151f490d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: afca952f75511ad4d5e0d6c9513fa68560fe5ed1843c445986fba36a3fca6fbf2fc192279f0341a917ac85ca89af19eb7c199b76ce57e93bed54875e3bd24f7d
|
|
7
|
+
data.tar.gz: eb9138a561acea12ed129f152425861e6405b3df999393c5dcf9bd99f5405a0f1a328dfee47c638a85333b1d71ff6e9ce9ebfaad954d84f9f984454a20958a53
|
data/CHANGELOG.md
CHANGED
|
@@ -70,7 +70,7 @@ NOTE: this version is a breaking change due to the removal of HPKP. Remove the H
|
|
|
70
70
|
|
|
71
71
|
## 5.0.0
|
|
72
72
|
|
|
73
|
-
Well this is a little
|
|
73
|
+
Well this is a little embarrassing. 4.0 was supposed to set the secure/httponly/samesite=lax attributes on cookies by default but it didn't. Now it does. - See the [upgrading to 5.0](docs/upgrading-to-5-0.md) guide.
|
|
74
74
|
|
|
75
75
|
## 4.0.1
|
|
76
76
|
|
|
@@ -194,7 +194,7 @@ end
|
|
|
194
194
|
|
|
195
195
|
## 3.4.0 the frame-src/child-src transition for Firefox.
|
|
196
196
|
|
|
197
|
-
Handle the `child-src`/`frame-src` transition semi-intelligently across versions. I think the code best
|
|
197
|
+
Handle the `child-src`/`frame-src` transition semi-intelligently across versions. I think the code best describes the behavior here:
|
|
198
198
|
|
|
199
199
|
```ruby
|
|
200
200
|
if supported_directives.include?(:child_src)
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Secure Headers ](https://github.com/github/secure_headers/actions/workflows/build.yml)
|
|
2
2
|
|
|
3
|
-
**main branch represents
|
|
3
|
+
**main branch represents 7.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), [upgrading to 6.x doc](docs/upgrading-to-6-0.md) or [upgrading to 7.x doc](docs/upgrading-to-7-0.md) for instructions on how to upgrade. Bug fixes should go in the `6.x` branch for now.
|
|
4
4
|
|
|
5
5
|
The gem will automatically apply several headers that are related to security. This includes:
|
|
6
6
|
- Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](https://www.w3.org/TR/CSP2/)
|
|
@@ -11,11 +11,11 @@ The gem will automatically apply several headers that are related to security.
|
|
|
11
11
|
- X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options Specification](https://tools.ietf.org/html/rfc7034)
|
|
12
12
|
- X-XSS-Protection - [Cross site scripting heuristic filter for IE/Chrome](https://msdn.microsoft.com/en-us/library/dd565647\(v=vs.85\).aspx)
|
|
13
13
|
- X-Content-Type-Options - [Prevent content type sniffing](https://msdn.microsoft.com/library/gg622941\(v=vs.85\).aspx)
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
14
|
+
- x-download-options - [Prevent file downloads opening](https://msdn.microsoft.com/library/jj542450(v=vs.85).aspx)
|
|
15
|
+
- x-permitted-cross-domain-policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html)
|
|
16
|
+
- referrer-policy - [Referrer Policy draft](https://w3c.github.io/webappsec-referrer-policy/)
|
|
17
|
+
- expect-ct - Only use certificates that are present in the certificate transparency logs. [expect-ct draft specification](https://datatracker.ietf.org/doc/draft-stark-expect-ct/).
|
|
18
|
+
- clear-site-data - Clearing browser data for origin. [clear-site-data specification](https://w3c.github.io/webappsec-clear-site-data/).
|
|
19
19
|
|
|
20
20
|
It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes. This is on default but can be turned off by using `config.cookies = SecureHeaders::OPT_OUT`.
|
|
21
21
|
|
|
@@ -88,9 +88,50 @@ SecureHeaders::Configuration.default do |config|
|
|
|
88
88
|
img_src: %w(somewhereelse.com),
|
|
89
89
|
report_uri: %w(https://report-uri.io/example-csp-report-only)
|
|
90
90
|
})
|
|
91
|
+
|
|
92
|
+
# Optional: Use the modern report-to directive (with Reporting-Endpoints header)
|
|
93
|
+
config.csp = config.csp.merge({
|
|
94
|
+
report_to: "csp-endpoint"
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
# When using report-to, configure the reporting endpoints header
|
|
98
|
+
config.reporting_endpoints = {
|
|
99
|
+
"csp-endpoint": "https://report-uri.io/example-csp",
|
|
100
|
+
"csp-report-only": "https://report-uri.io/example-csp-report-only"
|
|
101
|
+
}
|
|
91
102
|
end
|
|
92
103
|
```
|
|
93
104
|
|
|
105
|
+
### CSP Reporting
|
|
106
|
+
|
|
107
|
+
SecureHeaders supports both the legacy `report-uri` and the modern `report-to` directives for CSP violation reporting:
|
|
108
|
+
|
|
109
|
+
#### report-uri (Legacy)
|
|
110
|
+
The `report-uri` directive sends violations to a URL endpoint. It's widely supported but limited to POST requests with JSON payloads.
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
config.csp = {
|
|
114
|
+
default_src: %w('self'),
|
|
115
|
+
report_uri: %w(https://example.com/csp-report)
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### report-to (Modern)
|
|
120
|
+
The `report-to` directive specifies a named reporting endpoint defined in the `Reporting-Endpoints` header. This enables more flexible reporting through the HTTP Reporting API standard.
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
config.csp = {
|
|
124
|
+
default_src: %w('self'),
|
|
125
|
+
report_to: "csp-endpoint"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
config.reporting_endpoints = {
|
|
129
|
+
"csp-endpoint": "https://example.com/reports"
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Recommendation:** Use both `report-uri` and `report-to` for maximum compatibility while transitioning to the modern approach.
|
|
134
|
+
|
|
94
135
|
### Deprecated Configuration Values
|
|
95
136
|
* `block_all_mixed_content` - this value is deprecated in favor of `upgrade_insecure_requests`. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/block-all-mixed-content for more information.
|
|
96
137
|
|
|
@@ -99,13 +140,13 @@ end
|
|
|
99
140
|
All headers except for PublicKeyPins and ClearSiteData have a default value. The default set of headers is:
|
|
100
141
|
|
|
101
142
|
```
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
143
|
+
content-security-policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'
|
|
144
|
+
strict-transport-security: max-age=631138519
|
|
145
|
+
x-content-type-options: nosniff
|
|
146
|
+
x-download-options: noopen
|
|
147
|
+
x-frame-options: sameorigin
|
|
148
|
+
x-permitted-cross-domain-policies: none
|
|
149
|
+
x-xss-protection: 0
|
|
109
150
|
```
|
|
110
151
|
|
|
111
152
|
## API configurations
|
|
@@ -125,6 +166,29 @@ end
|
|
|
125
166
|
|
|
126
167
|
However, I would consider these headers anyways depending on your load and bandwidth requirements.
|
|
127
168
|
|
|
169
|
+
## Disabling secure_headers
|
|
170
|
+
|
|
171
|
+
If you want to disable `secure_headers` entirely (e.g., for specific environments or deployment scenarios), you can use `Configuration.disable!`:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
if ENV["ENABLE_STRICT_HEADERS"]
|
|
175
|
+
SecureHeaders::Configuration.default do |config|
|
|
176
|
+
# your configuration here
|
|
177
|
+
end
|
|
178
|
+
else
|
|
179
|
+
SecureHeaders::Configuration.disable!
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Important**: This configuration must be set during application startup (e.g., in an initializer). Once you call either `Configuration.default` or `Configuration.disable!`, the choice cannot be changed at runtime. Attempting to call `disable!` after `default` (or vice versa) will raise an `AlreadyConfiguredError`.
|
|
184
|
+
|
|
185
|
+
When disabled, no security headers will be set by the gem. This is useful when:
|
|
186
|
+
- You're gradually rolling out secure_headers across different customers or deployments
|
|
187
|
+
- You need to migrate existing custom headers to secure_headers
|
|
188
|
+
- You want environment-specific control over security headers
|
|
189
|
+
|
|
190
|
+
Note: When `disable!` is used, you don't need to configure a default configuration. The gem will not raise a `NotYetConfiguredError`.
|
|
191
|
+
|
|
128
192
|
## Acknowledgements
|
|
129
193
|
|
|
130
194
|
This project originated within the Security team at Twitter. An archived fork from the point of transition is here: https://github.com/twitter-archive/secure_headers.
|
|
@@ -9,23 +9,53 @@ module SecureHeaders
|
|
|
9
9
|
class NotYetConfiguredError < StandardError; end
|
|
10
10
|
class IllegalPolicyModificationError < StandardError; end
|
|
11
11
|
class << self
|
|
12
|
+
# Public: Disable secure_headers entirely. When disabled, no headers will be set.
|
|
13
|
+
#
|
|
14
|
+
# Note: This must be called before Configuration.default. Calling it after
|
|
15
|
+
# Configuration.default has been set will raise an AlreadyConfiguredError.
|
|
16
|
+
#
|
|
17
|
+
# Returns nothing
|
|
18
|
+
# Raises AlreadyConfiguredError if Configuration.default has already been called
|
|
19
|
+
def disable!
|
|
20
|
+
if defined?(@default_config)
|
|
21
|
+
raise AlreadyConfiguredError, "Configuration already set, cannot disable"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@disabled = true
|
|
25
|
+
@noop_config = create_noop_config.freeze
|
|
26
|
+
|
|
27
|
+
# Ensure the built-in NOOP override is available even if `default` has never been called
|
|
28
|
+
@overrides ||= {}
|
|
29
|
+
unless @overrides.key?(NOOP_OVERRIDE)
|
|
30
|
+
@overrides[NOOP_OVERRIDE] = method(:create_noop_config_block)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Public: Check if secure_headers is disabled
|
|
35
|
+
#
|
|
36
|
+
# Returns boolean
|
|
37
|
+
def disabled?
|
|
38
|
+
defined?(@disabled) && @disabled
|
|
39
|
+
end
|
|
40
|
+
|
|
12
41
|
# Public: Set the global default configuration.
|
|
13
42
|
#
|
|
14
43
|
# Optionally supply a block to override the defaults set by this library.
|
|
15
44
|
#
|
|
16
45
|
# Returns the newly created config.
|
|
46
|
+
# Raises AlreadyConfiguredError if Configuration.disable! has already been called
|
|
17
47
|
def default(&block)
|
|
48
|
+
if disabled?
|
|
49
|
+
raise AlreadyConfiguredError, "Configuration has been disabled, cannot set default"
|
|
50
|
+
end
|
|
51
|
+
|
|
18
52
|
if defined?(@default_config)
|
|
19
53
|
raise AlreadyConfiguredError, "Policy already configured"
|
|
20
54
|
end
|
|
21
55
|
|
|
22
56
|
# Define a built-in override that clears all configuration options and
|
|
23
57
|
# results in no security headers being set.
|
|
24
|
-
override(NOOP_OVERRIDE)
|
|
25
|
-
CONFIG_ATTRIBUTES.each do |attr|
|
|
26
|
-
config.instance_variable_set("@#{attr}", OPT_OUT)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
58
|
+
override(NOOP_OVERRIDE, &method(:create_noop_config_block))
|
|
29
59
|
|
|
30
60
|
new_config = new(&block).freeze
|
|
31
61
|
new_config.validate_config!
|
|
@@ -35,7 +65,7 @@ module SecureHeaders
|
|
|
35
65
|
|
|
36
66
|
# Public: create a named configuration that overrides the default config.
|
|
37
67
|
#
|
|
38
|
-
# name - use an
|
|
68
|
+
# name - use an identifier for the override config.
|
|
39
69
|
# base - override another existing config, or override the default config
|
|
40
70
|
# if no value is supplied.
|
|
41
71
|
#
|
|
@@ -101,6 +131,7 @@ module SecureHeaders
|
|
|
101
131
|
# of ensuring that the default config is never mutated and is dup(ed)
|
|
102
132
|
# before it is used in a request.
|
|
103
133
|
def default_config
|
|
134
|
+
return @noop_config if disabled?
|
|
104
135
|
unless defined?(@default_config)
|
|
105
136
|
raise NotYetConfiguredError, "Default policy not yet configured"
|
|
106
137
|
end
|
|
@@ -116,6 +147,19 @@ module SecureHeaders
|
|
|
116
147
|
value
|
|
117
148
|
end
|
|
118
149
|
end
|
|
150
|
+
|
|
151
|
+
# Private: Creates a NOOP configuration that opts out of all headers
|
|
152
|
+
def create_noop_config
|
|
153
|
+
new(&method(:create_noop_config_block))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Private: Block for creating NOOP configuration
|
|
157
|
+
# Used by both create_noop_config and the NOOP_OVERRIDE mechanism
|
|
158
|
+
def create_noop_config_block(config)
|
|
159
|
+
CONFIG_ATTRIBUTES.each do |attr|
|
|
160
|
+
config.instance_variable_set("@#{attr}", OPT_OUT)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
119
163
|
end
|
|
120
164
|
|
|
121
165
|
CONFIG_ATTRIBUTES_TO_HEADER_CLASSES = {
|
|
@@ -131,6 +175,7 @@ module SecureHeaders
|
|
|
131
175
|
csp: ContentSecurityPolicy,
|
|
132
176
|
csp_report_only: ContentSecurityPolicy,
|
|
133
177
|
cookies: Cookie,
|
|
178
|
+
reporting_endpoints: ReportingEndpoints,
|
|
134
179
|
}.freeze
|
|
135
180
|
|
|
136
181
|
CONFIG_ATTRIBUTES = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.keys.freeze
|
|
@@ -167,6 +212,7 @@ module SecureHeaders
|
|
|
167
212
|
@x_permitted_cross_domain_policies = nil
|
|
168
213
|
@x_xss_protection = nil
|
|
169
214
|
@expect_certificate_transparency = nil
|
|
215
|
+
@reporting_endpoints = nil
|
|
170
216
|
|
|
171
217
|
self.referrer_policy = OPT_OUT
|
|
172
218
|
self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT)
|
|
@@ -192,6 +238,7 @@ module SecureHeaders
|
|
|
192
238
|
copy.clear_site_data = @clear_site_data
|
|
193
239
|
copy.expect_certificate_transparency = @expect_certificate_transparency
|
|
194
240
|
copy.referrer_policy = @referrer_policy
|
|
241
|
+
copy.reporting_endpoints = self.class.send(:deep_copy_if_hash, @reporting_endpoints)
|
|
195
242
|
copy
|
|
196
243
|
end
|
|
197
244
|
|
|
@@ -256,7 +303,7 @@ module SecureHeaders
|
|
|
256
303
|
end
|
|
257
304
|
end
|
|
258
305
|
|
|
259
|
-
# Configures the
|
|
306
|
+
# Configures the content-security-policy-report-only header. `new_csp` cannot
|
|
260
307
|
# contain `report_only: false` or an error will be raised.
|
|
261
308
|
#
|
|
262
309
|
# NOTE: if csp has not been configured/has the default value when
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
module SecureHeaders
|
|
3
3
|
class ClearSiteDataConfigError < StandardError; end
|
|
4
4
|
class ClearSiteData
|
|
5
|
-
HEADER_NAME = "
|
|
5
|
+
HEADER_NAME = "clear-site-data".freeze
|
|
6
6
|
|
|
7
7
|
# Valid `types`
|
|
8
8
|
CACHE = "cache".freeze
|
|
@@ -11,43 +11,41 @@ module SecureHeaders
|
|
|
11
11
|
EXECUTION_CONTEXTS = "executionContexts".freeze
|
|
12
12
|
ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECUTION_CONTEXTS]
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
[HEADER_NAME, make_header_value(ALL_TYPES)]
|
|
26
|
-
end
|
|
14
|
+
# Public: make an clear-site-data header name, value pair
|
|
15
|
+
#
|
|
16
|
+
# Returns nil if not configured, returns header name and value if configured.
|
|
17
|
+
def self.make_header(config = nil, user_agent = nil)
|
|
18
|
+
case config
|
|
19
|
+
when nil, OPT_OUT, []
|
|
20
|
+
# noop
|
|
21
|
+
when Array
|
|
22
|
+
[HEADER_NAME, make_header_value(config)]
|
|
23
|
+
when true
|
|
24
|
+
[HEADER_NAME, make_header_value(ALL_TYPES)]
|
|
27
25
|
end
|
|
26
|
+
end
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end
|
|
37
|
-
else
|
|
38
|
-
raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`")
|
|
28
|
+
def self.validate_config!(config)
|
|
29
|
+
case config
|
|
30
|
+
when nil, OPT_OUT, true
|
|
31
|
+
# valid
|
|
32
|
+
when Array
|
|
33
|
+
unless config.all? { |t| t.is_a?(String) }
|
|
34
|
+
raise ClearSiteDataConfigError.new("types must be Strings")
|
|
39
35
|
end
|
|
36
|
+
else
|
|
37
|
+
raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`")
|
|
40
38
|
end
|
|
39
|
+
end
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
end
|
|
41
|
+
# Public: Transform a clear-site-data config (an Array of Strings) into a
|
|
42
|
+
# String that can be used as the value for the clear-site-data header.
|
|
43
|
+
#
|
|
44
|
+
# types - An Array of String of types of data to clear.
|
|
45
|
+
#
|
|
46
|
+
# Returns a String of quoted values that are comma separated.
|
|
47
|
+
def self.make_header_value(types)
|
|
48
|
+
types.map { |t| %("#{t}") }.join(", ")
|
|
51
49
|
end
|
|
52
50
|
end
|
|
53
51
|
end
|
|
@@ -26,8 +26,8 @@ module SecureHeaders
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
##
|
|
29
|
-
# Returns the name to use for the header. Either "
|
|
30
|
-
# "
|
|
29
|
+
# Returns the name to use for the header. Either "content-security-policy" or
|
|
30
|
+
# "content-security-policy-report-only"
|
|
31
31
|
def name
|
|
32
32
|
@config.class.const_get(:HEADER_NAME)
|
|
33
33
|
end
|
|
@@ -63,6 +63,8 @@ module SecureHeaders
|
|
|
63
63
|
build_sandbox_list_directive(directive_name)
|
|
64
64
|
when :media_type_list
|
|
65
65
|
build_media_type_list_directive(directive_name)
|
|
66
|
+
when :report_to_endpoint
|
|
67
|
+
build_report_to_directive(directive_name)
|
|
66
68
|
end
|
|
67
69
|
end.compact.join("; ")
|
|
68
70
|
end
|
|
@@ -79,7 +81,7 @@ module SecureHeaders
|
|
|
79
81
|
end
|
|
80
82
|
|
|
81
83
|
# A maximally strict sandbox policy is just the `sandbox` directive,
|
|
82
|
-
#
|
|
84
|
+
# with no configuration values.
|
|
83
85
|
if max_strict_policy
|
|
84
86
|
symbol_to_hyphen_case(directive)
|
|
85
87
|
elsif sandbox_list && sandbox_list.any?
|
|
@@ -100,6 +102,13 @@ module SecureHeaders
|
|
|
100
102
|
end
|
|
101
103
|
end
|
|
102
104
|
|
|
105
|
+
def build_report_to_directive(directive)
|
|
106
|
+
return unless endpoint_name = @config.directive_value(directive)
|
|
107
|
+
if endpoint_name && endpoint_name.is_a?(String) && !endpoint_name.empty?
|
|
108
|
+
[symbol_to_hyphen_case(directive), endpoint_name].join(" ")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
103
112
|
# Private: builds a string that represents one directive in a minified form.
|
|
104
113
|
#
|
|
105
114
|
# directive_name - a symbol representing the various ALL_DIRECTIVES
|
|
@@ -120,7 +129,7 @@ module SecureHeaders
|
|
|
120
129
|
end
|
|
121
130
|
|
|
122
131
|
# If a directive contains *, all other values are omitted.
|
|
123
|
-
# If a directive contains 'none' but has other values, 'none' is
|
|
132
|
+
# If a directive contains 'none' but has other values, 'none' is omitted.
|
|
124
133
|
# Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
|
|
125
134
|
def minify_source_list(directive, source_list)
|
|
126
135
|
source_list = source_list.compact
|
|
@@ -129,6 +138,7 @@ module SecureHeaders
|
|
|
129
138
|
else
|
|
130
139
|
source_list = populate_nonces(directive, source_list)
|
|
131
140
|
source_list = reject_all_values_if_none(source_list)
|
|
141
|
+
source_list = normalize_uri_paths(source_list)
|
|
132
142
|
|
|
133
143
|
unless directive == REPORT_URI || @preserve_schemes
|
|
134
144
|
source_list = strip_source_schemes(source_list)
|
|
@@ -151,6 +161,26 @@ module SecureHeaders
|
|
|
151
161
|
end
|
|
152
162
|
end
|
|
153
163
|
|
|
164
|
+
def normalize_uri_paths(source_list)
|
|
165
|
+
source_list.map do |source|
|
|
166
|
+
# Normalize domains ending in a single / as without omitting the slash accomplishes the same.
|
|
167
|
+
# https://www.w3.org/TR/CSP3/#match-paths § 6.6.2.10 Step 2
|
|
168
|
+
begin
|
|
169
|
+
uri = URI(source)
|
|
170
|
+
if uri.path == "/"
|
|
171
|
+
next source.chomp("/")
|
|
172
|
+
end
|
|
173
|
+
rescue URI::InvalidURIError
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if source.chomp("/").include?("/")
|
|
177
|
+
source
|
|
178
|
+
else
|
|
179
|
+
source.chomp("/")
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
154
184
|
# Private: append a nonce to the script/style directories if script_nonce
|
|
155
185
|
# or style_nonce are provided.
|
|
156
186
|
def populate_nonces(directive, source_list)
|
|
@@ -179,11 +209,12 @@ module SecureHeaders
|
|
|
179
209
|
end
|
|
180
210
|
|
|
181
211
|
# Private: return the list of directives,
|
|
182
|
-
# starting with default-src and ending with
|
|
212
|
+
# starting with default-src and ending with reporting directives (alphabetically ordered).
|
|
183
213
|
def directives
|
|
184
214
|
[
|
|
185
215
|
DEFAULT_SRC,
|
|
186
216
|
BODY_DIRECTIVES,
|
|
217
|
+
REPORT_TO,
|
|
187
218
|
REPORT_URI,
|
|
188
219
|
].flatten
|
|
189
220
|
end
|
|
@@ -78,7 +78,7 @@ module SecureHeaders
|
|
|
78
78
|
|
|
79
79
|
class ContentSecurityPolicyConfigError < StandardError; end
|
|
80
80
|
class ContentSecurityPolicyConfig
|
|
81
|
-
HEADER_NAME = "
|
|
81
|
+
HEADER_NAME = "content-security-policy".freeze
|
|
82
82
|
|
|
83
83
|
ATTRS = Set.new(PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES)
|
|
84
84
|
def self.attrs
|
|
@@ -107,7 +107,7 @@ module SecureHeaders
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig
|
|
110
|
-
HEADER_NAME = "
|
|
110
|
+
HEADER_NAME = "content-security-policy-report-only".freeze
|
|
111
111
|
|
|
112
112
|
def report_only?
|
|
113
113
|
true
|
|
@@ -7,10 +7,8 @@ module SecureHeaders
|
|
|
7
7
|
class CookiesConfigError < StandardError; end
|
|
8
8
|
class Cookie
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
CookiesConfig.new(config).validate!
|
|
13
|
-
end
|
|
10
|
+
def self.validate_config!(config)
|
|
11
|
+
CookiesConfig.new(config).validate!
|
|
14
12
|
end
|
|
15
13
|
|
|
16
14
|
attr_reader :raw_cookie, :config
|
|
@@ -3,37 +3,35 @@ module SecureHeaders
|
|
|
3
3
|
class ExpectCertificateTransparencyConfigError < StandardError; end
|
|
4
4
|
|
|
5
5
|
class ExpectCertificateTransparency
|
|
6
|
-
HEADER_NAME = "
|
|
6
|
+
HEADER_NAME = "expect-ct".freeze
|
|
7
7
|
INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze
|
|
8
8
|
INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze
|
|
9
9
|
REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze
|
|
10
10
|
INVALID_MAX_AGE_ERROR = "max-age must be a number.".freeze
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return if config.nil? || config == OPT_OUT
|
|
12
|
+
# Public: Generate a expect-ct header.
|
|
13
|
+
#
|
|
14
|
+
# Returns nil if not configured, returns header name and value if
|
|
15
|
+
# configured.
|
|
16
|
+
def self.make_header(config, use_agent = nil)
|
|
17
|
+
return if config.nil? || config == OPT_OUT
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
header = new(config)
|
|
20
|
+
[HEADER_NAME, header.value]
|
|
21
|
+
end
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
def self.validate_config!(config)
|
|
24
|
+
return if config.nil? || config == OPT_OUT
|
|
25
|
+
raise ExpectCertificateTransparencyConfigError.new(INVALID_CONFIGURATION_ERROR) unless config.is_a? Hash
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
unless [true, false, nil].include?(config[:enforce])
|
|
28
|
+
raise ExpectCertificateTransparencyConfigError.new(INVALID_ENFORCE_VALUE_ERROR)
|
|
29
|
+
end
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end
|
|
31
|
+
if !config[:max_age]
|
|
32
|
+
raise ExpectCertificateTransparencyConfigError.new(REQUIRED_MAX_AGE_ERROR)
|
|
33
|
+
elsif config[:max_age].to_s !~ /\A\d+\z/
|
|
34
|
+
raise ExpectCertificateTransparencyConfigError.new(INVALID_MAX_AGE_ERROR)
|
|
37
35
|
end
|
|
38
36
|
end
|
|
39
37
|
|
|
@@ -39,6 +39,7 @@ module SecureHeaders
|
|
|
39
39
|
SCRIPT_SRC = :script_src
|
|
40
40
|
STYLE_SRC = :style_src
|
|
41
41
|
REPORT_URI = :report_uri
|
|
42
|
+
REPORT_TO = :report_to
|
|
42
43
|
|
|
43
44
|
DIRECTIVES_1_0 = [
|
|
44
45
|
DEFAULT_SRC,
|
|
@@ -51,7 +52,8 @@ module SecureHeaders
|
|
|
51
52
|
SANDBOX,
|
|
52
53
|
SCRIPT_SRC,
|
|
53
54
|
STYLE_SRC,
|
|
54
|
-
REPORT_URI
|
|
55
|
+
REPORT_URI,
|
|
56
|
+
REPORT_TO
|
|
55
57
|
].freeze
|
|
56
58
|
|
|
57
59
|
BASE_URI = :base_uri
|
|
@@ -98,9 +100,9 @@ module SecureHeaders
|
|
|
98
100
|
|
|
99
101
|
# Experimental directives - these vary greatly in support
|
|
100
102
|
# See MDN for details.
|
|
101
|
-
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/
|
|
103
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/content-security-policy/trusted-types
|
|
102
104
|
TRUSTED_TYPES = :trusted_types
|
|
103
|
-
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/
|
|
105
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/content-security-policy/require-trusted-types-for
|
|
104
106
|
REQUIRE_TRUSTED_TYPES_FOR = :require_trusted_types_for
|
|
105
107
|
|
|
106
108
|
DIRECTIVES_EXPERIMENTAL = [
|
|
@@ -110,9 +112,9 @@ module SecureHeaders
|
|
|
110
112
|
|
|
111
113
|
ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_EXPERIMENTAL).uniq.sort
|
|
112
114
|
|
|
113
|
-
# Think of default-src and report-uri as the beginning and end respectively,
|
|
115
|
+
# Think of default-src and report-uri/report-to as the beginning and end respectively,
|
|
114
116
|
# everything else is in between.
|
|
115
|
-
BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
|
|
117
|
+
BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI, REPORT_TO]
|
|
116
118
|
|
|
117
119
|
DIRECTIVE_VALUE_TYPES = {
|
|
118
120
|
BASE_URI => :source_list,
|
|
@@ -129,10 +131,11 @@ module SecureHeaders
|
|
|
129
131
|
NAVIGATE_TO => :source_list,
|
|
130
132
|
OBJECT_SRC => :source_list,
|
|
131
133
|
PLUGIN_TYPES => :media_type_list,
|
|
134
|
+
PREFETCH_SRC => :source_list,
|
|
135
|
+
REPORT_TO => :report_to_endpoint,
|
|
136
|
+
REPORT_URI => :source_list,
|
|
132
137
|
REQUIRE_SRI_FOR => :require_sri_for_list,
|
|
133
138
|
REQUIRE_TRUSTED_TYPES_FOR => :require_trusted_types_for_list,
|
|
134
|
-
REPORT_URI => :source_list,
|
|
135
|
-
PREFETCH_SRC => :source_list,
|
|
136
139
|
SANDBOX => :sandbox_list,
|
|
137
140
|
SCRIPT_SRC => :source_list,
|
|
138
141
|
SCRIPT_SRC_ELEM => :source_list,
|
|
@@ -158,6 +161,7 @@ module SecureHeaders
|
|
|
158
161
|
FORM_ACTION,
|
|
159
162
|
FRAME_ANCESTORS,
|
|
160
163
|
NAVIGATE_TO,
|
|
164
|
+
REPORT_TO,
|
|
161
165
|
REPORT_URI,
|
|
162
166
|
]
|
|
163
167
|
|
|
@@ -201,7 +205,7 @@ module SecureHeaders
|
|
|
201
205
|
|
|
202
206
|
# Public: Validates each source expression.
|
|
203
207
|
#
|
|
204
|
-
# Does not validate the
|
|
208
|
+
# Does not validate the individual values of the source expression (e.g.
|
|
205
209
|
# script_src => h*t*t*p: will not raise an exception)
|
|
206
210
|
def validate_config!(config)
|
|
207
211
|
return if config.nil? || config.opt_out?
|
|
@@ -344,6 +348,8 @@ module SecureHeaders
|
|
|
344
348
|
validate_require_sri_source_expression!(directive, value)
|
|
345
349
|
when :require_trusted_types_for_list
|
|
346
350
|
validate_require_trusted_types_for_source_expression!(directive, value)
|
|
351
|
+
when :report_to_endpoint
|
|
352
|
+
validate_report_to_endpoint_expression!(directive, value)
|
|
347
353
|
else
|
|
348
354
|
raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
|
|
349
355
|
end
|
|
@@ -398,11 +404,23 @@ module SecureHeaders
|
|
|
398
404
|
end
|
|
399
405
|
end
|
|
400
406
|
|
|
407
|
+
# Private: validates that a report-to endpoint expression:
|
|
408
|
+
# 1. is a string
|
|
409
|
+
# 2. is not empty
|
|
410
|
+
def validate_report_to_endpoint_expression!(directive, endpoint_name)
|
|
411
|
+
unless endpoint_name.is_a?(String)
|
|
412
|
+
raise ContentSecurityPolicyConfigError.new("#{directive} must be a string. Found #{endpoint_name.class} value")
|
|
413
|
+
end
|
|
414
|
+
if endpoint_name.empty?
|
|
415
|
+
raise ContentSecurityPolicyConfigError.new("#{directive} must not be empty")
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
401
419
|
# Private: validates that a source expression:
|
|
402
420
|
# 1. is an array of strings
|
|
403
421
|
# 2. does not contain any deprecated, now invalid values (inline, eval, self, none)
|
|
404
422
|
#
|
|
405
|
-
# Does not validate the
|
|
423
|
+
# Does not validate the individual values of the source expression (e.g.
|
|
406
424
|
# script_src => h*t*t*p: will not raise an exception)
|
|
407
425
|
def validate_source_expression!(directive, source_expression)
|
|
408
426
|
if source_expression != OPT_OUT
|