secure_headers 4.0.0.alpha01 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -1
- data/README.md +9 -0
- data/docs/per_action_configuration.md +28 -0
- data/lib/secure_headers/configuration.rb +4 -1
- data/lib/secure_headers/headers/clear_site_data.rb +2 -2
- data/lib/secure_headers/headers/content_security_policy.rb +45 -8
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
- data/lib/secure_headers/headers/policy_management.rb +95 -48
- data/lib/secure_headers/middleware.rb +1 -1
- data/lib/secure_headers.rb +3 -1
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +31 -9
- data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +36 -0
- data/spec/lib/secure_headers/middleware_spec.rb +11 -0
- data/spec/spec_helper.rb +1 -0
- data/upgrading-to-4-0.md +6 -0
- metadata +7 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: de4e0f18558ac4a65a9983f56e4dd65ca95e5f82
|
|
4
|
+
data.tar.gz: 3c16a5bdeec36aab812bcbfbbdba544b969c3a33
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 14f3bc42066b1b4bd6044b75a78818a6ce6edc60760e84af03d13623758a4d05b9397d0c4b4baa92c71d105e0334fa39ec56124c3014e8a63096236f9c6e6d51
|
|
7
|
+
data.tar.gz: f07cfc4d11b24a7d7ec1ad50b2fb756fd3a69c8558b922eb79daf35bbf5dc721c24ca27dfa8b5ca13865dc3345cb0162161f3c41fa04ffbff8c190908d8a7b32
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
## 4.x
|
|
2
2
|
|
|
3
|
-
- See the [upgrading to 4.0](upgrading-to-4
|
|
3
|
+
- See the [upgrading to 4.0](upgrading-to-4-0.md) guide. Lots of breaking changes.
|
|
4
|
+
|
|
5
|
+
## 3.7.1
|
|
6
|
+
|
|
7
|
+
Fix support for the sandbox attribute of CSP. `true` and `[]` represent the maximally restricted policy (`sandbox;`) and validate other values.
|
|
8
|
+
|
|
9
|
+
## 3.7.0
|
|
10
|
+
|
|
11
|
+
Adds support for the `Expect-CT` header (@jacobbednarz: https://github.com/twitter/secureheaders/pull/322)
|
|
12
|
+
|
|
13
|
+
## 3.6.7
|
|
14
|
+
|
|
15
|
+
Actually set manifest-src when configured. https://github.com/twitter/secureheaders/pull/339 Thanks @carlosantoniodasilva!
|
|
4
16
|
|
|
5
17
|
## 3.6.5
|
|
6
18
|
|
data/README.md
CHANGED
|
@@ -19,6 +19,7 @@ The gem will automatically apply several headers that are related to security.
|
|
|
19
19
|
- 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)
|
|
20
20
|
- Referrer-Policy - [Referrer Policy draft](https://w3c.github.io/webappsec-referrer-policy/)
|
|
21
21
|
- Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469)
|
|
22
|
+
- 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/).
|
|
22
23
|
- Clear-Site-Data - Clearing browser data for origin. [Clear-Site-Data specification](https://w3c.github.io/webappsec-clear-site-data/).
|
|
23
24
|
|
|
24
25
|
It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so).
|
|
@@ -54,6 +55,8 @@ If you do not supply a `default` configuration, exceptions will be raised. If yo
|
|
|
54
55
|
|
|
55
56
|
All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` will disable the header entirely.
|
|
56
57
|
|
|
58
|
+
**Word of caution:** The following is not a default configuration per se. It serves as a sample implementation of the configuration. You should read more about these headers and determine what is appropriate for your requirements.
|
|
59
|
+
|
|
57
60
|
```ruby
|
|
58
61
|
SecureHeaders::Configuration.default do |config|
|
|
59
62
|
config.cookies = {
|
|
@@ -77,6 +80,11 @@ SecureHeaders::Configuration.default do |config|
|
|
|
77
80
|
"storage",
|
|
78
81
|
"executionContexts"
|
|
79
82
|
]
|
|
83
|
+
config.expect_certificate_transparency = {
|
|
84
|
+
enforce: false,
|
|
85
|
+
max_age: 1.day.to_i,
|
|
86
|
+
report_uri: "https://report-uri.io/example-ct"
|
|
87
|
+
}
|
|
80
88
|
config.csp = {
|
|
81
89
|
# "meta" values. these will shape the header, but the values are not included in the header.
|
|
82
90
|
preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
|
|
@@ -94,6 +102,7 @@ SecureHeaders::Configuration.default do |config|
|
|
|
94
102
|
manifest_src: %w('self'),
|
|
95
103
|
media_src: %w(utoob.com),
|
|
96
104
|
object_src: %w('self'),
|
|
105
|
+
sandbox: true, # true and [] will set a maximally restrictive setting
|
|
97
106
|
plugin_types: %w(application/x-shockwave-flash),
|
|
98
107
|
script_src: %w('self'),
|
|
99
108
|
style_src: %w('unsafe-inline'),
|
|
@@ -103,3 +103,31 @@ Content-Security-Policy: ...
|
|
|
103
103
|
console.log("won't execute, not whitelisted")
|
|
104
104
|
</script>
|
|
105
105
|
```
|
|
106
|
+
|
|
107
|
+
## Clearing browser cache
|
|
108
|
+
|
|
109
|
+
You can clear the browser cache after the logout request by using the following.
|
|
110
|
+
|
|
111
|
+
``` ruby
|
|
112
|
+
class ApplicationController < ActionController::Base
|
|
113
|
+
# Configuration override to send the Clear-Site-Data header.
|
|
114
|
+
SecureHeaders::Configuration.override(:clear_browser_cache) do |config|
|
|
115
|
+
config.clear_site_data = [
|
|
116
|
+
SecureHeaders::ClearSiteData::ALL_TYPES
|
|
117
|
+
]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# Clears the browser's cache for browsers supporting the Clear-Site-Data
|
|
122
|
+
# header.
|
|
123
|
+
#
|
|
124
|
+
# Returns nothing.
|
|
125
|
+
def clear_browser_cache
|
|
126
|
+
SecureHeaders.use_secure_headers_override(request, :clear_browser_cache)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
class SessionsController < ApplicationController
|
|
131
|
+
after_action :clear_browser_cache, only: :destroy
|
|
132
|
+
end
|
|
133
|
+
```
|
|
@@ -117,7 +117,7 @@ module SecureHeaders
|
|
|
117
117
|
|
|
118
118
|
attr_writer :hsts, :x_frame_options, :x_content_type_options,
|
|
119
119
|
:x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
|
|
120
|
-
:referrer_policy, :clear_site_data
|
|
120
|
+
:referrer_policy, :clear_site_data, :expect_certificate_transparency
|
|
121
121
|
|
|
122
122
|
attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
|
|
123
123
|
|
|
@@ -144,6 +144,7 @@ module SecureHeaders
|
|
|
144
144
|
@x_frame_options = nil
|
|
145
145
|
@x_permitted_cross_domain_policies = nil
|
|
146
146
|
@x_xss_protection = nil
|
|
147
|
+
@expect_certificate_transparency = nil
|
|
147
148
|
|
|
148
149
|
self.hpkp = OPT_OUT
|
|
149
150
|
self.referrer_policy = OPT_OUT
|
|
@@ -169,6 +170,7 @@ module SecureHeaders
|
|
|
169
170
|
copy.x_download_options = @x_download_options
|
|
170
171
|
copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
|
|
171
172
|
copy.clear_site_data = @clear_site_data
|
|
173
|
+
copy.expect_certificate_transparency = @expect_certificate_transparency
|
|
172
174
|
copy.referrer_policy = @referrer_policy
|
|
173
175
|
copy.hpkp = @hpkp
|
|
174
176
|
copy.hpkp_report_host = @hpkp_report_host
|
|
@@ -201,6 +203,7 @@ module SecureHeaders
|
|
|
201
203
|
XDownloadOptions.validate_config!(@x_download_options)
|
|
202
204
|
XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
|
|
203
205
|
ClearSiteData.validate_config!(@clear_site_data)
|
|
206
|
+
ExpectCertificateTransparency.validate_config!(@expect_certificate_transparency)
|
|
204
207
|
PublicKeyPins.validate_config!(@hpkp)
|
|
205
208
|
Cookie.validate_config!(@cookies)
|
|
206
209
|
end
|
|
@@ -8,8 +8,8 @@ module SecureHeaders
|
|
|
8
8
|
CACHE = "cache".freeze
|
|
9
9
|
COOKIES = "cookies".freeze
|
|
10
10
|
STORAGE = "storage".freeze
|
|
11
|
-
|
|
12
|
-
ALL_TYPES = [CACHE, COOKIES, STORAGE,
|
|
11
|
+
EXECUTION_CONTEXTS = "executionContexts".freeze
|
|
12
|
+
ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECUTION_CONTEXTS]
|
|
13
13
|
|
|
14
14
|
CONFIG_KEY = :clear_site_data
|
|
15
15
|
|
|
@@ -75,20 +75,55 @@ module SecureHeaders
|
|
|
75
75
|
case DIRECTIVE_VALUE_TYPES[directive_name]
|
|
76
76
|
when :boolean
|
|
77
77
|
symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
|
|
78
|
-
when :
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
when :sandbox_list
|
|
79
|
+
build_sandbox_list_directive(directive_name)
|
|
80
|
+
when :media_type_list
|
|
81
|
+
build_media_type_list_directive(directive_name)
|
|
82
|
+
when :source_list
|
|
83
|
+
build_source_list_directive(directive_name)
|
|
82
84
|
end
|
|
83
85
|
end.compact.join("; ")
|
|
84
86
|
end
|
|
85
87
|
|
|
88
|
+
def build_sandbox_list_directive(directive)
|
|
89
|
+
return unless sandbox_list = @config.directive_value(directive)
|
|
90
|
+
max_strict_policy = case sandbox_list
|
|
91
|
+
when Array
|
|
92
|
+
sandbox_list.empty?
|
|
93
|
+
when true
|
|
94
|
+
true
|
|
95
|
+
else
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A maximally strict sandbox policy is just the `sandbox` directive,
|
|
100
|
+
# whith no configuraiton values.
|
|
101
|
+
if max_strict_policy
|
|
102
|
+
symbol_to_hyphen_case(directive)
|
|
103
|
+
elsif sandbox_list && sandbox_list.any?
|
|
104
|
+
[
|
|
105
|
+
symbol_to_hyphen_case(directive),
|
|
106
|
+
sandbox_list.uniq
|
|
107
|
+
].join(" ")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def build_media_type_list_directive(directive)
|
|
112
|
+
return unless media_type_list = @config.directive_value(directive)
|
|
113
|
+
if media_type_list && media_type_list.any?
|
|
114
|
+
[
|
|
115
|
+
symbol_to_hyphen_case(directive),
|
|
116
|
+
media_type_list.uniq
|
|
117
|
+
].join(" ")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
86
121
|
# Private: builds a string that represents one directive in a minified form.
|
|
87
122
|
#
|
|
88
123
|
# directive_name - a symbol representing the various ALL_DIRECTIVES
|
|
89
124
|
#
|
|
90
125
|
# Returns a string representing a directive.
|
|
91
|
-
def
|
|
126
|
+
def build_source_list_directive(directive)
|
|
92
127
|
source_list = case directive
|
|
93
128
|
when :child_src
|
|
94
129
|
if supported_directives.include?(:child_src)
|
|
@@ -101,9 +136,11 @@ module SecureHeaders
|
|
|
101
136
|
else
|
|
102
137
|
@config.directive_value(directive)
|
|
103
138
|
end
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
139
|
+
|
|
140
|
+
if source_list != OPT_OUT && source_list && source_list.any?
|
|
141
|
+
normalized_source_list = minify_source_list(directive, source_list)
|
|
142
|
+
[symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
|
|
143
|
+
end
|
|
107
144
|
end
|
|
108
145
|
|
|
109
146
|
# If a directive contains *, all other values are omitted.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module SecureHeaders
|
|
3
|
+
class ExpectCertificateTransparencyConfigError < StandardError; end
|
|
4
|
+
|
|
5
|
+
class ExpectCertificateTransparency
|
|
6
|
+
HEADER_NAME = "Expect-CT".freeze
|
|
7
|
+
CONFIG_KEY = :expect_certificate_transparency
|
|
8
|
+
INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze
|
|
9
|
+
INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze
|
|
10
|
+
REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze
|
|
11
|
+
INVALID_MAX_AGE_ERROR = "max-age must be a number.".freeze
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Public: Generate a Expect-CT header.
|
|
15
|
+
#
|
|
16
|
+
# Returns nil if not configured, returns header name and value if
|
|
17
|
+
# configured.
|
|
18
|
+
def make_header(config)
|
|
19
|
+
return if config.nil?
|
|
20
|
+
|
|
21
|
+
header = new(config)
|
|
22
|
+
[HEADER_NAME, header.value]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate_config!(config)
|
|
26
|
+
return if config.nil? || config == OPT_OUT
|
|
27
|
+
raise ExpectCertificateTransparencyConfigError.new(INVALID_CONFIGURATION_ERROR) unless config.is_a? Hash
|
|
28
|
+
|
|
29
|
+
unless [true, false, nil].include?(config[:enforce])
|
|
30
|
+
raise ExpectCertificateTransparencyConfigError.new(INVALID_ENFORCE_VALUE_ERROR)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if !config[:max_age]
|
|
34
|
+
raise ExpectCertificateTransparencyConfigError.new(REQUIRED_MAX_AGE_ERROR)
|
|
35
|
+
elsif config[:max_age].to_s !~ /\A\d+\z/
|
|
36
|
+
raise ExpectCertificateTransparencyConfigError.new(INVALID_MAX_AGE_ERROR)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(config)
|
|
42
|
+
@enforced = config.fetch(:enforce, nil)
|
|
43
|
+
@max_age = config.fetch(:max_age, nil)
|
|
44
|
+
@report_uri = config.fetch(:report_uri, nil)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def value
|
|
48
|
+
[
|
|
49
|
+
enforced_directive,
|
|
50
|
+
max_age_directive,
|
|
51
|
+
report_uri_directive
|
|
52
|
+
].compact.join("; ").strip
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def enforced_directive
|
|
56
|
+
# Unfortunately `if @enforced` isn't enough here in case someone
|
|
57
|
+
# passes in a random string so let's be specific with it to prevent
|
|
58
|
+
# accidental enforcement.
|
|
59
|
+
"enforce" if @enforced == true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def max_age_directive
|
|
63
|
+
"max-age=#{@max_age}" if @max_age
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def report_uri_directive
|
|
67
|
+
"report-uri=\"#{@report_uri}\"" if @report_uri
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -116,18 +116,6 @@ module SecureHeaders
|
|
|
116
116
|
# everything else is in between.
|
|
117
117
|
BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
|
|
118
118
|
|
|
119
|
-
# These are directives that do not inherit the default-src value. This is
|
|
120
|
-
# useful when calling #combine_policies.
|
|
121
|
-
NON_FETCH_SOURCES = [
|
|
122
|
-
BASE_URI,
|
|
123
|
-
FORM_ACTION,
|
|
124
|
-
FRAME_ANCESTORS,
|
|
125
|
-
PLUGIN_TYPES,
|
|
126
|
-
REPORT_URI
|
|
127
|
-
]
|
|
128
|
-
|
|
129
|
-
FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES
|
|
130
|
-
|
|
131
119
|
VARIATIONS = {
|
|
132
120
|
"Chrome" => CHROME_DIRECTIVES,
|
|
133
121
|
"Opera" => CHROME_DIRECTIVES,
|
|
@@ -155,14 +143,30 @@ module SecureHeaders
|
|
|
155
143
|
MANIFEST_SRC => :source_list,
|
|
156
144
|
MEDIA_SRC => :source_list,
|
|
157
145
|
OBJECT_SRC => :source_list,
|
|
158
|
-
PLUGIN_TYPES => :
|
|
146
|
+
PLUGIN_TYPES => :media_type_list,
|
|
159
147
|
REPORT_URI => :source_list,
|
|
160
|
-
SANDBOX => :
|
|
148
|
+
SANDBOX => :sandbox_list,
|
|
161
149
|
SCRIPT_SRC => :source_list,
|
|
162
150
|
STYLE_SRC => :source_list,
|
|
163
151
|
UPGRADE_INSECURE_REQUESTS => :boolean
|
|
164
152
|
}.freeze
|
|
165
153
|
|
|
154
|
+
# These are directives that don't have use a source list, and hence do not
|
|
155
|
+
# inherit the default-src value.
|
|
156
|
+
NON_SOURCE_LIST_SOURCES = DIRECTIVE_VALUE_TYPES.select do |_, type|
|
|
157
|
+
type != :source_list
|
|
158
|
+
end.keys.freeze
|
|
159
|
+
|
|
160
|
+
# These are directives that take a source list, but that do not inherit
|
|
161
|
+
# the default-src value.
|
|
162
|
+
NON_FETCH_SOURCES = [
|
|
163
|
+
BASE_URI,
|
|
164
|
+
FORM_ACTION,
|
|
165
|
+
FRAME_ANCESTORS,
|
|
166
|
+
REPORT_URI
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES - NON_SOURCE_LIST_SOURCES
|
|
166
170
|
|
|
167
171
|
STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
|
|
168
172
|
HTTP_SCHEME_REGEX = %r{\Ahttps?://}
|
|
@@ -202,7 +206,10 @@ module SecureHeaders
|
|
|
202
206
|
def validate_config!(config)
|
|
203
207
|
return if config.nil? || config.opt_out?
|
|
204
208
|
raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src)
|
|
205
|
-
|
|
209
|
+
if config.directive_value(:script_src).nil?
|
|
210
|
+
raise ContentSecurityPolicyConfigError.new(":script_src is required, falling back to default-src is too dangerous. Use `script_src: OPT_OUT` to override")
|
|
211
|
+
end
|
|
212
|
+
|
|
206
213
|
ContentSecurityPolicyConfig.attrs.each do |key|
|
|
207
214
|
value = config.directive_value(key)
|
|
208
215
|
next unless value
|
|
@@ -261,7 +268,7 @@ module SecureHeaders
|
|
|
261
268
|
# when each hash contains a value for a given key.
|
|
262
269
|
def merge_policy_additions(original, additions)
|
|
263
270
|
original.merge(additions) do |directive, lhs, rhs|
|
|
264
|
-
if
|
|
271
|
+
if list_directive?(directive)
|
|
265
272
|
(lhs.to_a + rhs.to_a).compact.uniq
|
|
266
273
|
else
|
|
267
274
|
rhs
|
|
@@ -269,20 +276,27 @@ module SecureHeaders
|
|
|
269
276
|
end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
|
|
270
277
|
end
|
|
271
278
|
|
|
279
|
+
# Returns True if a directive expects a list of values and False otherwise.
|
|
280
|
+
def list_directive?(directive)
|
|
281
|
+
source_list?(directive) ||
|
|
282
|
+
sandbox_list?(directive) ||
|
|
283
|
+
media_type_list?(directive)
|
|
284
|
+
end
|
|
285
|
+
|
|
272
286
|
# For each directive in additions that does not exist in the original config,
|
|
273
287
|
# copy the default-src value to the original config. This modifies the original hash.
|
|
274
288
|
def populate_fetch_source_with_default!(original, additions)
|
|
275
289
|
# in case we would be appending to an empty directive, fill it with the default-src value
|
|
276
290
|
additions.each_key do |directive|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
291
|
+
directive = if directive.to_s.end_with?("_nonce")
|
|
292
|
+
directive.to_s.gsub(/_nonce/, "_src").to_sym
|
|
293
|
+
else
|
|
294
|
+
directive
|
|
295
|
+
end
|
|
296
|
+
# Don't set a default if directive has an existing value
|
|
297
|
+
next if original[directive]
|
|
298
|
+
if FETCH_SOURCES.include?(directive)
|
|
299
|
+
original[directive] = default_for(directive, original)
|
|
286
300
|
end
|
|
287
301
|
end
|
|
288
302
|
end
|
|
@@ -293,45 +307,77 @@ module SecureHeaders
|
|
|
293
307
|
original[DEFAULT_SRC]
|
|
294
308
|
end
|
|
295
309
|
|
|
296
|
-
def nonce_added?(original, additions)
|
|
297
|
-
[:script_nonce, :style_nonce].each do |nonce|
|
|
298
|
-
if additions[nonce] && !original[nonce]
|
|
299
|
-
return true
|
|
300
|
-
end
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
|
|
304
310
|
def source_list?(directive)
|
|
305
311
|
DIRECTIVE_VALUE_TYPES[directive] == :source_list
|
|
306
312
|
end
|
|
307
313
|
|
|
314
|
+
def sandbox_list?(directive)
|
|
315
|
+
DIRECTIVE_VALUE_TYPES[directive] == :sandbox_list
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def media_type_list?(directive)
|
|
319
|
+
DIRECTIVE_VALUE_TYPES[directive] == :media_type_list
|
|
320
|
+
end
|
|
321
|
+
|
|
308
322
|
# Private: Validates that the configuration has a valid type, or that it is a valid
|
|
309
323
|
# source expression.
|
|
310
|
-
def validate_directive!(directive,
|
|
324
|
+
def validate_directive!(directive, value)
|
|
325
|
+
ensure_valid_directive!(directive)
|
|
311
326
|
case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive]
|
|
312
327
|
when :boolean
|
|
313
|
-
unless boolean?(
|
|
314
|
-
raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean value")
|
|
315
|
-
end
|
|
316
|
-
when :string
|
|
317
|
-
unless source_expression.is_a?(String)
|
|
318
|
-
raise ContentSecurityPolicyConfigError.new("#{directive} Must be a string. Found #{config.class}: #{config} value")
|
|
328
|
+
unless boolean?(value)
|
|
329
|
+
raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean. Found #{value.class} value")
|
|
319
330
|
end
|
|
331
|
+
when :sandbox_list
|
|
332
|
+
validate_sandbox_expression!(directive, value)
|
|
333
|
+
when :media_type_list
|
|
334
|
+
validate_media_type_expression!(directive, value)
|
|
335
|
+
when :source_list
|
|
336
|
+
validate_source_expression!(directive, value)
|
|
320
337
|
else
|
|
321
|
-
|
|
338
|
+
raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Private: validates that a sandbox token expression:
|
|
343
|
+
# 1. is an array of strings or optionally `true` (to enable maximal sandboxing)
|
|
344
|
+
# 2. For arrays, each element is of the form allow-*
|
|
345
|
+
def validate_sandbox_expression!(directive, sandbox_token_expression)
|
|
346
|
+
# We support sandbox: true to indicate a maximally secure sandbox.
|
|
347
|
+
return if boolean?(sandbox_token_expression) && sandbox_token_expression == true
|
|
348
|
+
ensure_array_of_strings!(directive, sandbox_token_expression)
|
|
349
|
+
valid = sandbox_token_expression.compact.all? do |v|
|
|
350
|
+
v.is_a?(String) && v.start_with?("allow-")
|
|
351
|
+
end
|
|
352
|
+
if !valid
|
|
353
|
+
raise ContentSecurityPolicyConfigError.new("#{directive} must be True or an array of zero or more sandbox token strings (ex. allow-forms)")
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Private: validates that a media type expression:
|
|
358
|
+
# 1. is an array of strings
|
|
359
|
+
# 2. each element is of the form type/subtype
|
|
360
|
+
def validate_media_type_expression!(directive, media_type_expression)
|
|
361
|
+
ensure_array_of_strings!(directive, media_type_expression)
|
|
362
|
+
valid = media_type_expression.compact.all? do |v|
|
|
363
|
+
# All media types are of the form: <type from RFC 2045> "/" <subtype from RFC 2045>.
|
|
364
|
+
v =~ /\A.+\/.+\z/
|
|
365
|
+
end
|
|
366
|
+
if !valid
|
|
367
|
+
raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of valid media types (ex. application/pdf)")
|
|
322
368
|
end
|
|
323
369
|
end
|
|
324
370
|
|
|
325
371
|
# Private: validates that a source expression:
|
|
326
|
-
# 1.
|
|
327
|
-
# 2.
|
|
328
|
-
# 3. does not contain any depreated, now invalid values (inline, eval, self, none)
|
|
372
|
+
# 1. is an array of strings
|
|
373
|
+
# 2. does not contain any deprecated, now invalid values (inline, eval, self, none)
|
|
329
374
|
#
|
|
330
375
|
# Does not validate the invididual values of the source expression (e.g.
|
|
331
376
|
# script_src => h*t*t*p: will not raise an exception)
|
|
332
377
|
def validate_source_expression!(directive, source_expression)
|
|
333
|
-
|
|
334
|
-
|
|
378
|
+
if source_expression != OPT_OUT
|
|
379
|
+
ensure_array_of_strings!(directive, source_expression)
|
|
380
|
+
end
|
|
335
381
|
ensure_valid_sources!(directive, source_expression)
|
|
336
382
|
end
|
|
337
383
|
|
|
@@ -341,13 +387,14 @@ module SecureHeaders
|
|
|
341
387
|
end
|
|
342
388
|
end
|
|
343
389
|
|
|
344
|
-
def ensure_array_of_strings!(directive,
|
|
345
|
-
|
|
390
|
+
def ensure_array_of_strings!(directive, value)
|
|
391
|
+
if (!value.is_a?(Array) || !value.compact.all? { |v| v.is_a?(String) })
|
|
346
392
|
raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of strings")
|
|
347
393
|
end
|
|
348
394
|
end
|
|
349
395
|
|
|
350
396
|
def ensure_valid_sources!(directive, source_expression)
|
|
397
|
+
return if source_expression == OPT_OUT
|
|
351
398
|
source_expression.each do |expression|
|
|
352
399
|
if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(expression)
|
|
353
400
|
raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{expression}). This value must be single quoted.")
|
data/lib/secure_headers.rb
CHANGED
|
@@ -12,6 +12,7 @@ require "secure_headers/headers/x_download_options"
|
|
|
12
12
|
require "secure_headers/headers/x_permitted_cross_domain_policies"
|
|
13
13
|
require "secure_headers/headers/referrer_policy"
|
|
14
14
|
require "secure_headers/headers/clear_site_data"
|
|
15
|
+
require "secure_headers/headers/expect_certificate_transparency"
|
|
15
16
|
require "secure_headers/middleware"
|
|
16
17
|
require "secure_headers/railtie"
|
|
17
18
|
require "secure_headers/view_helper"
|
|
@@ -24,7 +25,7 @@ module SecureHeaders
|
|
|
24
25
|
class NoOpHeaderConfig
|
|
25
26
|
include Singleton
|
|
26
27
|
|
|
27
|
-
def boom(
|
|
28
|
+
def boom(*args)
|
|
28
29
|
raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead."
|
|
29
30
|
end
|
|
30
31
|
|
|
@@ -52,6 +53,7 @@ module SecureHeaders
|
|
|
52
53
|
CSP = ContentSecurityPolicy
|
|
53
54
|
|
|
54
55
|
ALL_HEADER_CLASSES = [
|
|
56
|
+
ExpectCertificateTransparency,
|
|
55
57
|
ClearSiteData,
|
|
56
58
|
ContentSecurityPolicyConfig,
|
|
57
59
|
ContentSecurityPolicyReportOnlyConfig,
|
data/secure_headers.gemspec
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
Gem::Specification.new do |gem|
|
|
4
4
|
gem.name = "secure_headers"
|
|
5
|
-
gem.version = "4.0.0
|
|
5
|
+
gem.version = "4.0.0"
|
|
6
6
|
gem.authors = ["Neil Matatall"]
|
|
7
7
|
gem.email = ["neil.matatall@gmail.com"]
|
|
8
8
|
gem.description = "Manages application of security headers with many safe defaults."
|
|
@@ -51,6 +51,11 @@ module SecureHeaders
|
|
|
51
51
|
expect(csp.value).to eq("default-src example.org")
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
it "does not build directives with a value of OPT_OUT (and bypasses directive requirements)" do
|
|
55
|
+
csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), script_src: OPT_OUT)
|
|
56
|
+
expect(csp.value).to eq("default-src example.org")
|
|
57
|
+
end
|
|
58
|
+
|
|
54
59
|
it "does not remove schemes from report-uri values" do
|
|
55
60
|
csp = ContentSecurityPolicy.new(default_src: %w(https:), report_uri: %w(https://example.org))
|
|
56
61
|
expect(csp.value).to eq("default-src https:; report-uri https://example.org")
|
|
@@ -91,6 +96,21 @@ module SecureHeaders
|
|
|
91
96
|
expect(csp.value).to eq("default-src example.org")
|
|
92
97
|
end
|
|
93
98
|
|
|
99
|
+
it "creates maximally strict sandbox policy when passed no sandbox token values" do
|
|
100
|
+
csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: [])
|
|
101
|
+
expect(csp.value).to eq("default-src example.org; sandbox")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "creates maximally strict sandbox policy when passed true" do
|
|
105
|
+
csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: true)
|
|
106
|
+
expect(csp.value).to eq("default-src example.org; sandbox")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "creates sandbox policy when passed valid sandbox token values" do
|
|
110
|
+
csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: %w(allow-forms allow-scripts))
|
|
111
|
+
expect(csp.value).to eq("default-src example.org; sandbox allow-forms allow-scripts")
|
|
112
|
+
end
|
|
113
|
+
|
|
94
114
|
it "does not emit a warning when using frame-src" do
|
|
95
115
|
expect(Kernel).to_not receive(:warn)
|
|
96
116
|
ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
|
|
@@ -115,50 +135,52 @@ module SecureHeaders
|
|
|
115
135
|
block_all_mixed_content: true,
|
|
116
136
|
upgrade_insecure_requests: true,
|
|
117
137
|
script_src: %w(script-src.com),
|
|
118
|
-
script_nonce: 123456
|
|
138
|
+
script_nonce: 123456,
|
|
139
|
+
sandbox: %w(allow-forms),
|
|
140
|
+
plugin_types: %w(application/pdf)
|
|
119
141
|
})
|
|
120
142
|
end
|
|
121
143
|
|
|
122
144
|
it "does not filter any directives for Chrome" do
|
|
123
145
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
|
|
124
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types
|
|
146
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
125
147
|
end
|
|
126
148
|
|
|
127
149
|
it "does not filter any directives for Opera" do
|
|
128
150
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
|
|
129
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types
|
|
151
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
130
152
|
end
|
|
131
153
|
|
|
132
154
|
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
|
|
133
155
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
|
|
134
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox
|
|
156
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
135
157
|
end
|
|
136
158
|
|
|
137
159
|
it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
|
|
138
160
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
|
|
139
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox
|
|
161
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
140
162
|
end
|
|
141
163
|
|
|
142
164
|
it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for Edge" do
|
|
143
165
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
|
|
144
|
-
expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox
|
|
166
|
+
expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
145
167
|
end
|
|
146
168
|
|
|
147
169
|
it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do
|
|
148
170
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
|
|
149
|
-
expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox
|
|
171
|
+
expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
150
172
|
end
|
|
151
173
|
|
|
152
174
|
it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do
|
|
153
175
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10])
|
|
154
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types
|
|
176
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; report-uri report-uri.com")
|
|
155
177
|
end
|
|
156
178
|
|
|
157
179
|
it "falls back to standard Firefox defaults when the useragent version is not present" do
|
|
158
180
|
ua = USER_AGENTS[:firefox].dup
|
|
159
181
|
allow(ua).to receive(:version).and_return(nil)
|
|
160
182
|
policy = ContentSecurityPolicy.new(complex_opts, ua)
|
|
161
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox
|
|
183
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
162
184
|
end
|
|
163
185
|
end
|
|
164
186
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
3
|
+
|
|
4
|
+
module SecureHeaders
|
|
5
|
+
describe ExpectCertificateTransparency do
|
|
6
|
+
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: true).value).to eq("enforce; max-age=1234") }
|
|
7
|
+
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: false).value).to eq("max-age=1234") }
|
|
8
|
+
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: "yolocopter").value).to eq("max-age=1234") }
|
|
9
|
+
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, report_uri: "https://report-uri.io/expect-ct").value).to eq("max-age=1234; report-uri=\"https://report-uri.io/expect-ct\"") }
|
|
10
|
+
specify do
|
|
11
|
+
config = { enforce: true, max_age: 1234, report_uri: "https://report-uri.io/expect-ct" }
|
|
12
|
+
header_value = "enforce; max-age=1234; report-uri=\"https://report-uri.io/expect-ct\""
|
|
13
|
+
expect(ExpectCertificateTransparency.new(config).value).to eq(header_value)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context "with an invalid configuration" do
|
|
17
|
+
it "raises an exception when configuration isn't a hash" do
|
|
18
|
+
expect do
|
|
19
|
+
ExpectCertificateTransparency.validate_config!(%w(a))
|
|
20
|
+
end.to raise_error(ExpectCertificateTransparencyConfigError)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "raises an exception when max-age is not provided" do
|
|
24
|
+
expect do
|
|
25
|
+
ExpectCertificateTransparency.validate_config!(foo: "bar")
|
|
26
|
+
end.to raise_error(ExpectCertificateTransparencyConfigError)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "raises an exception with an invalid max-age" do
|
|
30
|
+
expect do
|
|
31
|
+
ExpectCertificateTransparency.validate_config!(max_age: "abc123")
|
|
32
|
+
end.to raise_error(ExpectCertificateTransparencyConfigError)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "raises an exception with an invalid enforce value" do
|
|
36
|
+
expect do
|
|
37
|
+
ExpectCertificateTransparency.validate_config!(enforce: "brokenstring")
|
|
38
|
+
end.to raise_error(ExpectCertificateTransparencyConfigError)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -57,6 +57,12 @@ module SecureHeaders
|
|
|
57
57
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
it "accepts OPT_OUT as a script-src value" do
|
|
61
|
+
expect do
|
|
62
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), script_src: OPT_OUT))
|
|
63
|
+
end.to_not raise_error
|
|
64
|
+
end
|
|
65
|
+
|
|
60
66
|
it "requires :report_only to be a truthy value" do
|
|
61
67
|
expect do
|
|
62
68
|
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: "steve")))
|
|
@@ -105,6 +111,36 @@ module SecureHeaders
|
|
|
105
111
|
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval), script_src: %w('self')))
|
|
106
112
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
107
113
|
end
|
|
114
|
+
|
|
115
|
+
it "rejects anything not of the form allow-* as a sandbox value" do
|
|
116
|
+
expect do
|
|
117
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: ["steve"])))
|
|
118
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "accepts anything of the form allow-* as a sandbox value " do
|
|
122
|
+
expect do
|
|
123
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: ["allow-foo"])))
|
|
124
|
+
end.to_not raise_error
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "accepts true as a sandbox policy" do
|
|
128
|
+
expect do
|
|
129
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: true)))
|
|
130
|
+
end.to_not raise_error
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "rejects anything not of the form type/subtype as a plugin-type value" do
|
|
134
|
+
expect do
|
|
135
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["steve"])))
|
|
136
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "accepts anything of the form type/subtype as a plugin-type value " do
|
|
140
|
+
expect do
|
|
141
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["application/pdf"])))
|
|
142
|
+
end.to_not raise_error
|
|
143
|
+
end
|
|
108
144
|
end
|
|
109
145
|
|
|
110
146
|
describe "#combine_policies" do
|
|
@@ -65,6 +65,17 @@ module SecureHeaders
|
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
it "allows opting out of cookie protection with OPT_OUT alone" do
|
|
69
|
+
Configuration.default { |config| config.cookies = OPT_OUT}
|
|
70
|
+
|
|
71
|
+
# do NOT make this request https. non-https requests modify a config,
|
|
72
|
+
# causing an exception when operating on OPT_OUT. This ensures we don't
|
|
73
|
+
# try to modify the config.
|
|
74
|
+
request = Rack::Request.new({})
|
|
75
|
+
_, env = cookie_middleware.call request.env
|
|
76
|
+
expect(env["Set-Cookie"]).to eq("foo=bar")
|
|
77
|
+
end
|
|
78
|
+
|
|
68
79
|
context "cookies should not be flagged" do
|
|
69
80
|
it "does not flags cookies as secure" do
|
|
70
81
|
Configuration.default { |config| config.cookies = {secure: OPT_OUT, httponly: OPT_OUT, samesite: OPT_OUT} }
|
data/spec/spec_helper.rb
CHANGED
|
@@ -33,6 +33,7 @@ def expect_default_values(hash)
|
|
|
33
33
|
expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE)
|
|
34
34
|
expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE)
|
|
35
35
|
expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil
|
|
36
|
+
expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
module SecureHeaders
|
data/upgrading-to-4-0.md
CHANGED
|
@@ -18,6 +18,12 @@ config.cookies = {
|
|
|
18
18
|
config.cookies = OPT_OUT
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## script_src must be set
|
|
22
|
+
|
|
23
|
+
Not setting a `script_src` value means your policy falls back to whatever `default_src` (also required) is set to. This can be very dangerous and indicates the policy is too loose.
|
|
24
|
+
|
|
25
|
+
However, sometimes you really don't need a `script-src` e.g. API responses (`default-src 'none'`) so you can set `script_src: SecureHeaders::OPT_OUT` to work around this.
|
|
26
|
+
|
|
21
27
|
## Default Content Security Policy
|
|
22
28
|
|
|
23
29
|
The default CSP has changed to be more universal without sacrificing too much security.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: secure_headers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.0.0
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Neil Matatall
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2017-
|
|
11
|
+
date: 2017-09-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -74,6 +74,7 @@ files:
|
|
|
74
74
|
- lib/secure_headers/headers/content_security_policy.rb
|
|
75
75
|
- lib/secure_headers/headers/content_security_policy_config.rb
|
|
76
76
|
- lib/secure_headers/headers/cookie.rb
|
|
77
|
+
- lib/secure_headers/headers/expect_certificate_transparency.rb
|
|
77
78
|
- lib/secure_headers/headers/policy_management.rb
|
|
78
79
|
- lib/secure_headers/headers/public_key_pins.rb
|
|
79
80
|
- lib/secure_headers/headers/referrer_policy.rb
|
|
@@ -93,6 +94,7 @@ files:
|
|
|
93
94
|
- spec/lib/secure_headers/headers/clear_site_data_spec.rb
|
|
94
95
|
- spec/lib/secure_headers/headers/content_security_policy_spec.rb
|
|
95
96
|
- spec/lib/secure_headers/headers/cookie_spec.rb
|
|
97
|
+
- spec/lib/secure_headers/headers/expect_certificate_spec.rb
|
|
96
98
|
- spec/lib/secure_headers/headers/policy_management_spec.rb
|
|
97
99
|
- spec/lib/secure_headers/headers/public_key_pins_spec.rb
|
|
98
100
|
- spec/lib/secure_headers/headers/referrer_policy_spec.rb
|
|
@@ -128,9 +130,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
128
130
|
version: '0'
|
|
129
131
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
130
132
|
requirements:
|
|
131
|
-
- - "
|
|
133
|
+
- - ">="
|
|
132
134
|
- !ruby/object:Gem::Version
|
|
133
|
-
version:
|
|
135
|
+
version: '0'
|
|
134
136
|
requirements: []
|
|
135
137
|
rubyforge_project:
|
|
136
138
|
rubygems_version: 2.6.11
|
|
@@ -143,6 +145,7 @@ test_files:
|
|
|
143
145
|
- spec/lib/secure_headers/headers/clear_site_data_spec.rb
|
|
144
146
|
- spec/lib/secure_headers/headers/content_security_policy_spec.rb
|
|
145
147
|
- spec/lib/secure_headers/headers/cookie_spec.rb
|
|
148
|
+
- spec/lib/secure_headers/headers/expect_certificate_spec.rb
|
|
146
149
|
- spec/lib/secure_headers/headers/policy_management_spec.rb
|
|
147
150
|
- spec/lib/secure_headers/headers/public_key_pins_spec.rb
|
|
148
151
|
- spec/lib/secure_headers/headers/referrer_policy_spec.rb
|