secure_headers 6.7.0 → 7.3.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 +61 -13
- 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 +14 -16
- 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 +16 -4
- metadata +32 -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 -24
- 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/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: a859861ca3a93245cbfa9c421b209c44800ff3b5bd92c26591bdd99dde7efd30
|
|
4
|
+
data.tar.gz: 8576b10d0855d453cf42ba9335cbe8deeb8f82258b04ada1c2f2ab01a04ae373
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 95d65823683591d7c3a4c26bc99b6849f16b4caecbd9c5c1fd73ffe22e75311cf2124d6ce1fd25670dd4c11120ce01dbb46cd003c15387f3bfebfe4592c3542f
|
|
7
|
+
data.tar.gz: ef35df794270845ab6807c50a9e37e1832c3e4e5293098038f2caf6b3635aae024a907df99ff9e5e4e0580fb7a7995d0d63f6d9b9e0452aca22f024b7be297e9
|
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,13 +81,13 @@ 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?
|
|
86
88
|
[
|
|
87
89
|
symbol_to_hyphen_case(directive),
|
|
88
|
-
sandbox_list.uniq
|
|
90
|
+
scrub_directive_value(directive, sandbox_list.uniq.join(" "))
|
|
89
91
|
].join(" ")
|
|
90
92
|
end
|
|
91
93
|
end
|
|
@@ -95,11 +97,40 @@ module SecureHeaders
|
|
|
95
97
|
if media_type_list && media_type_list.any?
|
|
96
98
|
[
|
|
97
99
|
symbol_to_hyphen_case(directive),
|
|
98
|
-
media_type_list.uniq
|
|
100
|
+
scrub_directive_value(directive, media_type_list.uniq.join(" "))
|
|
99
101
|
].join(" ")
|
|
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), scrub_directive_value(directive, endpoint_name)].join(" ")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Bytes that would let a caller-controlled value break out of its
|
|
113
|
+
# directive and inject sibling CSP directives. CR/LF are included
|
|
114
|
+
# so naive downstreams that split on bare \r can't be used to
|
|
115
|
+
# smuggle directives either.
|
|
116
|
+
DIRECTIVE_INJECTION_REGEX = /[\n\r;]/.freeze
|
|
117
|
+
|
|
118
|
+
# Private: scrubs caller-controlled bytes that would let a value
|
|
119
|
+
# break out of its CSP directive (CR, LF, semicolon). Shared across
|
|
120
|
+
# every directive builder so sandbox / plugin-types / report-to /
|
|
121
|
+
# source-list all reject the same byte set with the same warn UX.
|
|
122
|
+
# Emits a single Kernel.warn per directive even when multiple
|
|
123
|
+
# offending bytes are present.
|
|
124
|
+
def scrub_directive_value(directive, value)
|
|
125
|
+
str = value.to_s
|
|
126
|
+
if str =~ DIRECTIVE_INJECTION_REGEX
|
|
127
|
+
Kernel.warn("#{directive} contains a #{$~[0].inspect} in #{str.inspect} which will raise an error in future versions. It has been replaced with a blank space.")
|
|
128
|
+
str.gsub(DIRECTIVE_INJECTION_REGEX, " ")
|
|
129
|
+
else
|
|
130
|
+
str
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
103
134
|
# Private: builds a string that represents one directive in a minified form.
|
|
104
135
|
#
|
|
105
136
|
# directive_name - a symbol representing the various ALL_DIRECTIVES
|
|
@@ -109,18 +140,13 @@ module SecureHeaders
|
|
|
109
140
|
source_list = @config.directive_value(directive)
|
|
110
141
|
if source_list != OPT_OUT && source_list && source_list.any?
|
|
111
142
|
minified_source_list = minify_source_list(directive, source_list).join(" ")
|
|
112
|
-
|
|
113
|
-
if minified_source_list =~ /(\n|;)/
|
|
114
|
-
Kernel.warn("#{directive} contains a #{$1} in #{minified_source_list.inspect} which will raise an error in future versions. It has been replaced with a blank space.")
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
escaped_source_list = minified_source_list.gsub(/[\n;]/, " ")
|
|
143
|
+
escaped_source_list = scrub_directive_value(directive, minified_source_list)
|
|
118
144
|
[symbol_to_hyphen_case(directive), escaped_source_list].join(" ").strip
|
|
119
145
|
end
|
|
120
146
|
end
|
|
121
147
|
|
|
122
148
|
# If a directive contains *, all other values are omitted.
|
|
123
|
-
# If a directive contains 'none' but has other values, 'none' is
|
|
149
|
+
# If a directive contains 'none' but has other values, 'none' is omitted.
|
|
124
150
|
# Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
|
|
125
151
|
def minify_source_list(directive, source_list)
|
|
126
152
|
source_list = source_list.compact
|
|
@@ -129,6 +155,7 @@ module SecureHeaders
|
|
|
129
155
|
else
|
|
130
156
|
source_list = populate_nonces(directive, source_list)
|
|
131
157
|
source_list = reject_all_values_if_none(source_list)
|
|
158
|
+
source_list = normalize_uri_paths(source_list)
|
|
132
159
|
|
|
133
160
|
unless directive == REPORT_URI || @preserve_schemes
|
|
134
161
|
source_list = strip_source_schemes(source_list)
|
|
@@ -151,6 +178,26 @@ module SecureHeaders
|
|
|
151
178
|
end
|
|
152
179
|
end
|
|
153
180
|
|
|
181
|
+
def normalize_uri_paths(source_list)
|
|
182
|
+
source_list.map do |source|
|
|
183
|
+
# Normalize domains ending in a single / as without omitting the slash accomplishes the same.
|
|
184
|
+
# https://www.w3.org/TR/CSP3/#match-paths § 6.6.2.10 Step 2
|
|
185
|
+
begin
|
|
186
|
+
uri = URI(source)
|
|
187
|
+
if uri.path == "/"
|
|
188
|
+
next source.chomp("/")
|
|
189
|
+
end
|
|
190
|
+
rescue URI::InvalidURIError
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
if source.chomp("/").include?("/")
|
|
194
|
+
source
|
|
195
|
+
else
|
|
196
|
+
source.chomp("/")
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
154
201
|
# Private: append a nonce to the script/style directories if script_nonce
|
|
155
202
|
# or style_nonce are provided.
|
|
156
203
|
def populate_nonces(directive, source_list)
|
|
@@ -179,11 +226,12 @@ module SecureHeaders
|
|
|
179
226
|
end
|
|
180
227
|
|
|
181
228
|
# Private: return the list of directives,
|
|
182
|
-
# starting with default-src and ending with
|
|
229
|
+
# starting with default-src and ending with reporting directives (alphabetically ordered).
|
|
183
230
|
def directives
|
|
184
231
|
[
|
|
185
232
|
DEFAULT_SRC,
|
|
186
233
|
BODY_DIRECTIVES,
|
|
234
|
+
REPORT_TO,
|
|
187
235
|
REPORT_URI,
|
|
188
236
|
].flatten
|
|
189
237
|
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
|
|