secure_headers 6.3.4 → 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 +10 -2
- data/Gemfile +2 -0
- data/README.md +81 -15
- data/lib/secure_headers/configuration.rb +64 -13
- data/lib/secure_headers/headers/clear_site_data.rb +31 -33
- data/lib/secure_headers/headers/content_security_policy.rb +80 -46
- data/lib/secure_headers/headers/content_security_policy_config.rb +17 -58
- data/lib/secure_headers/headers/cookie.rb +4 -6
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +20 -22
- data/lib/secure_headers/headers/policy_management.rb +66 -18
- 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/view_helper.rb +7 -6
- data/lib/secure_headers.rb +26 -2
- data/lib/tasks/tasks.rake +4 -53
- data/secure_headers.gemspec +19 -7
- metadata +38 -70
- data/.github/ISSUE_TEMPLATE.md +0 -41
- data/.github/PULL_REQUEST_TEMPLATE.md +0 -20
- 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 -190
- 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 -264
- 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 -191
- 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
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 6.5.0
|
|
2
|
+
|
|
3
|
+
- CSP: Remove source expression deduplication. (@lgarron) https://github.com/github/secure_headers/pull/499
|
|
4
|
+
|
|
5
|
+
## 6.4.0
|
|
6
|
+
|
|
7
|
+
- CSP: Add support for trusted-types, require-trusted-types-for directive (@JackMc): https://github.com/github/secure_headers/pull/486
|
|
8
|
+
|
|
1
9
|
## 6.3.4
|
|
2
10
|
|
|
3
11
|
- CSP: Do not deduplicate alternate schema source expressions (@keithamus): https://github.com/github/secure_headers/pull/478
|
|
@@ -62,7 +70,7 @@ NOTE: this version is a breaking change due to the removal of HPKP. Remove the H
|
|
|
62
70
|
|
|
63
71
|
## 5.0.0
|
|
64
72
|
|
|
65
|
-
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.
|
|
66
74
|
|
|
67
75
|
## 4.0.1
|
|
68
76
|
|
|
@@ -186,7 +194,7 @@ end
|
|
|
186
194
|
|
|
187
195
|
## 3.4.0 the frame-src/child-src transition for Firefox.
|
|
188
196
|
|
|
189
|
-
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:
|
|
190
198
|
|
|
191
199
|
```ruby
|
|
192
200
|
if supported_directives.include?(:child_src)
|
data/Gemfile
CHANGED
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
|
|
|
@@ -62,7 +62,6 @@ SecureHeaders::Configuration.default do |config|
|
|
|
62
62
|
# directive values: these values will directly translate into source directives
|
|
63
63
|
default_src: %w('none'),
|
|
64
64
|
base_uri: %w('self'),
|
|
65
|
-
block_all_mixed_content: true, # see https://www.w3.org/TR/mixed-content/
|
|
66
65
|
child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set.
|
|
67
66
|
connect_src: %w(wss:),
|
|
68
67
|
font_src: %w('self' data:),
|
|
@@ -89,21 +88,65 @@ SecureHeaders::Configuration.default do |config|
|
|
|
89
88
|
img_src: %w(somewhereelse.com),
|
|
90
89
|
report_uri: %w(https://report-uri.io/example-csp-report-only)
|
|
91
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
|
+
}
|
|
92
102
|
end
|
|
93
103
|
```
|
|
94
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
|
+
|
|
135
|
+
### Deprecated Configuration Values
|
|
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.
|
|
137
|
+
|
|
95
138
|
## Default values
|
|
96
139
|
|
|
97
140
|
All headers except for PublicKeyPins and ClearSiteData have a default value. The default set of headers is:
|
|
98
141
|
|
|
99
142
|
```
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
107
150
|
```
|
|
108
151
|
|
|
109
152
|
## API configurations
|
|
@@ -123,6 +166,29 @@ end
|
|
|
123
166
|
|
|
124
167
|
However, I would consider these headers anyways depending on your load and bandwidth requirements.
|
|
125
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
|
+
|
|
126
192
|
## Acknowledgements
|
|
127
193
|
|
|
128
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
|
#
|
|
@@ -83,13 +113,17 @@ module SecureHeaders
|
|
|
83
113
|
# can lead to modifying parent objects.
|
|
84
114
|
def deep_copy(config)
|
|
85
115
|
return unless config
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
116
|
+
result = {}
|
|
117
|
+
config.each_pair do |key, value|
|
|
118
|
+
result[key] =
|
|
119
|
+
case value
|
|
120
|
+
when Array
|
|
121
|
+
value.dup
|
|
122
|
+
else
|
|
123
|
+
value
|
|
124
|
+
end
|
|
92
125
|
end
|
|
126
|
+
result
|
|
93
127
|
end
|
|
94
128
|
|
|
95
129
|
# Private: Returns the internal default configuration. This should only
|
|
@@ -97,6 +131,7 @@ module SecureHeaders
|
|
|
97
131
|
# of ensuring that the default config is never mutated and is dup(ed)
|
|
98
132
|
# before it is used in a request.
|
|
99
133
|
def default_config
|
|
134
|
+
return @noop_config if disabled?
|
|
100
135
|
unless defined?(@default_config)
|
|
101
136
|
raise NotYetConfiguredError, "Default policy not yet configured"
|
|
102
137
|
end
|
|
@@ -112,6 +147,19 @@ module SecureHeaders
|
|
|
112
147
|
value
|
|
113
148
|
end
|
|
114
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
|
|
115
163
|
end
|
|
116
164
|
|
|
117
165
|
CONFIG_ATTRIBUTES_TO_HEADER_CLASSES = {
|
|
@@ -127,6 +175,7 @@ module SecureHeaders
|
|
|
127
175
|
csp: ContentSecurityPolicy,
|
|
128
176
|
csp_report_only: ContentSecurityPolicy,
|
|
129
177
|
cookies: Cookie,
|
|
178
|
+
reporting_endpoints: ReportingEndpoints,
|
|
130
179
|
}.freeze
|
|
131
180
|
|
|
132
181
|
CONFIG_ATTRIBUTES = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES.keys.freeze
|
|
@@ -163,6 +212,7 @@ module SecureHeaders
|
|
|
163
212
|
@x_permitted_cross_domain_policies = nil
|
|
164
213
|
@x_xss_protection = nil
|
|
165
214
|
@expect_certificate_transparency = nil
|
|
215
|
+
@reporting_endpoints = nil
|
|
166
216
|
|
|
167
217
|
self.referrer_policy = OPT_OUT
|
|
168
218
|
self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT)
|
|
@@ -188,6 +238,7 @@ module SecureHeaders
|
|
|
188
238
|
copy.clear_site_data = @clear_site_data
|
|
189
239
|
copy.expect_certificate_transparency = @expect_certificate_transparency
|
|
190
240
|
copy.referrer_policy = @referrer_policy
|
|
241
|
+
copy.reporting_endpoints = self.class.send(:deep_copy_if_hash, @reporting_endpoints)
|
|
191
242
|
copy
|
|
192
243
|
end
|
|
193
244
|
|
|
@@ -252,7 +303,7 @@ module SecureHeaders
|
|
|
252
303
|
end
|
|
253
304
|
end
|
|
254
305
|
|
|
255
|
-
# Configures the
|
|
306
|
+
# Configures the content-security-policy-report-only header. `new_csp` cannot
|
|
256
307
|
# contain `report_only: false` or an error will be raised.
|
|
257
308
|
#
|
|
258
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
|
|
@@ -7,26 +7,27 @@ module SecureHeaders
|
|
|
7
7
|
include PolicyManagement
|
|
8
8
|
|
|
9
9
|
def initialize(config = nil)
|
|
10
|
-
@config =
|
|
11
|
-
if config
|
|
12
|
-
|
|
10
|
+
@config =
|
|
11
|
+
if config.is_a?(Hash)
|
|
12
|
+
if config[:report_only]
|
|
13
|
+
ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
|
|
14
|
+
else
|
|
15
|
+
ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG)
|
|
16
|
+
end
|
|
17
|
+
elsif config.nil?
|
|
18
|
+
ContentSecurityPolicyConfig.new(DEFAULT_CONFIG)
|
|
13
19
|
else
|
|
14
|
-
|
|
20
|
+
config
|
|
15
21
|
end
|
|
16
|
-
elsif config.nil?
|
|
17
|
-
ContentSecurityPolicyConfig.new(DEFAULT_CONFIG)
|
|
18
|
-
else
|
|
19
|
-
config
|
|
20
|
-
end
|
|
21
22
|
|
|
22
|
-
@preserve_schemes = @config
|
|
23
|
-
@script_nonce = @config
|
|
24
|
-
@style_nonce = @config
|
|
23
|
+
@preserve_schemes = @config[:preserve_schemes]
|
|
24
|
+
@script_nonce = @config[:script_nonce]
|
|
25
|
+
@style_nonce = @config[:style_nonce]
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
##
|
|
28
|
-
# Returns the name to use for the header. Either "
|
|
29
|
-
# "
|
|
29
|
+
# Returns the name to use for the header. Either "content-security-policy" or
|
|
30
|
+
# "content-security-policy-report-only"
|
|
30
31
|
def name
|
|
31
32
|
@config.class.const_get(:HEADER_NAME)
|
|
32
33
|
end
|
|
@@ -34,11 +35,12 @@ module SecureHeaders
|
|
|
34
35
|
##
|
|
35
36
|
# Return the value of the CSP header
|
|
36
37
|
def value
|
|
37
|
-
@value ||=
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
@value ||=
|
|
39
|
+
if @config
|
|
40
|
+
build_value
|
|
41
|
+
else
|
|
42
|
+
DEFAULT_VALUE
|
|
43
|
+
end
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
private
|
|
@@ -51,7 +53,9 @@ module SecureHeaders
|
|
|
51
53
|
def build_value
|
|
52
54
|
directives.map do |directive_name|
|
|
53
55
|
case DIRECTIVE_VALUE_TYPES[directive_name]
|
|
54
|
-
when :source_list,
|
|
56
|
+
when :source_list,
|
|
57
|
+
:require_sri_for_list, # require_sri is a simple set of strings that don't need to deal with symbol casing
|
|
58
|
+
:require_trusted_types_for_list
|
|
55
59
|
build_source_list_directive(directive_name)
|
|
56
60
|
when :boolean
|
|
57
61
|
symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
|
|
@@ -59,6 +63,8 @@ module SecureHeaders
|
|
|
59
63
|
build_sandbox_list_directive(directive_name)
|
|
60
64
|
when :media_type_list
|
|
61
65
|
build_media_type_list_directive(directive_name)
|
|
66
|
+
when :report_to_endpoint
|
|
67
|
+
build_report_to_directive(directive_name)
|
|
62
68
|
end
|
|
63
69
|
end.compact.join("; ")
|
|
64
70
|
end
|
|
@@ -75,13 +81,13 @@ module SecureHeaders
|
|
|
75
81
|
end
|
|
76
82
|
|
|
77
83
|
# A maximally strict sandbox policy is just the `sandbox` directive,
|
|
78
|
-
#
|
|
84
|
+
# with no configuration values.
|
|
79
85
|
if max_strict_policy
|
|
80
86
|
symbol_to_hyphen_case(directive)
|
|
81
87
|
elsif sandbox_list && sandbox_list.any?
|
|
82
88
|
[
|
|
83
89
|
symbol_to_hyphen_case(directive),
|
|
84
|
-
sandbox_list.uniq
|
|
90
|
+
scrub_directive_value(directive, sandbox_list.uniq.join(" "))
|
|
85
91
|
].join(" ")
|
|
86
92
|
end
|
|
87
93
|
end
|
|
@@ -91,11 +97,40 @@ module SecureHeaders
|
|
|
91
97
|
if media_type_list && media_type_list.any?
|
|
92
98
|
[
|
|
93
99
|
symbol_to_hyphen_case(directive),
|
|
94
|
-
media_type_list.uniq
|
|
100
|
+
scrub_directive_value(directive, media_type_list.uniq.join(" "))
|
|
95
101
|
].join(" ")
|
|
96
102
|
end
|
|
97
103
|
end
|
|
98
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
|
+
|
|
99
134
|
# Private: builds a string that represents one directive in a minified form.
|
|
100
135
|
#
|
|
101
136
|
# directive_name - a symbol representing the various ALL_DIRECTIVES
|
|
@@ -105,18 +140,13 @@ module SecureHeaders
|
|
|
105
140
|
source_list = @config.directive_value(directive)
|
|
106
141
|
if source_list != OPT_OUT && source_list && source_list.any?
|
|
107
142
|
minified_source_list = minify_source_list(directive, source_list).join(" ")
|
|
108
|
-
|
|
109
|
-
if minified_source_list =~ /(\n|;)/
|
|
110
|
-
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.")
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
escaped_source_list = minified_source_list.gsub(/[\n;]/, " ")
|
|
143
|
+
escaped_source_list = scrub_directive_value(directive, minified_source_list)
|
|
114
144
|
[symbol_to_hyphen_case(directive), escaped_source_list].join(" ").strip
|
|
115
145
|
end
|
|
116
146
|
end
|
|
117
147
|
|
|
118
148
|
# If a directive contains *, all other values are omitted.
|
|
119
|
-
# If a directive contains 'none' but has other values, 'none' is
|
|
149
|
+
# If a directive contains 'none' but has other values, 'none' is omitted.
|
|
120
150
|
# Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
|
|
121
151
|
def minify_source_list(directive, source_list)
|
|
122
152
|
source_list = source_list.compact
|
|
@@ -125,11 +155,12 @@ module SecureHeaders
|
|
|
125
155
|
else
|
|
126
156
|
source_list = populate_nonces(directive, source_list)
|
|
127
157
|
source_list = reject_all_values_if_none(source_list)
|
|
158
|
+
source_list = normalize_uri_paths(source_list)
|
|
128
159
|
|
|
129
160
|
unless directive == REPORT_URI || @preserve_schemes
|
|
130
161
|
source_list = strip_source_schemes(source_list)
|
|
131
162
|
end
|
|
132
|
-
|
|
163
|
+
source_list.uniq
|
|
133
164
|
end
|
|
134
165
|
end
|
|
135
166
|
|
|
@@ -147,21 +178,23 @@ module SecureHeaders
|
|
|
147
178
|
end
|
|
148
179
|
end
|
|
149
180
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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("/")
|
|
162
197
|
end
|
|
163
|
-
else
|
|
164
|
-
sources
|
|
165
198
|
end
|
|
166
199
|
end
|
|
167
200
|
|
|
@@ -193,11 +226,12 @@ module SecureHeaders
|
|
|
193
226
|
end
|
|
194
227
|
|
|
195
228
|
# Private: return the list of directives,
|
|
196
|
-
# starting with default-src and ending with
|
|
229
|
+
# starting with default-src and ending with reporting directives (alphabetically ordered).
|
|
197
230
|
def directives
|
|
198
231
|
[
|
|
199
232
|
DEFAULT_SRC,
|
|
200
233
|
BODY_DIRECTIVES,
|
|
234
|
+
REPORT_TO,
|
|
201
235
|
REPORT_URI,
|
|
202
236
|
].flatten
|
|
203
237
|
end
|