secure_headers 4.0.0.alpha03 → 4.0.0.alpha04
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.
Potentially problematic release.
This version of secure_headers might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/README.md +7 -0
- data/lib/secure_headers.rb +2 -0
- data/lib/secure_headers/configuration.rb +4 -1
- data/lib/secure_headers/headers/content_security_policy.rb +40 -5
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
- data/lib/secure_headers/headers/policy_management.rb +90 -47
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +26 -9
- data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +30 -0
- data/spec/spec_helper.rb +1 -0
- data/upgrading-to-4-0.md +6 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 96b8466fa2b4e4400b0c587bdba7abebdac251f7
|
4
|
+
data.tar.gz: f721a9682de31fc461dd713e153cba427f846b51
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 29125ef959986803eff014bfd101972752272c97a5170356961dde54b78c4bcafdb5dc6fc55f99cd49f46b5c29c247e63161da9af6932fdb136d690f1dc2a598
|
7
|
+
data.tar.gz: b23ef45067291c21c66b735ebe19c454e8b35b5d829324ea35e33a4cb8018fad7e2a53081508334d9483bb7ff86cd9ce90b053f1c4b9052bad8c0d991d9d7f6e
|
data/CHANGELOG.md
CHANGED
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).
|
@@ -77,6 +78,11 @@ SecureHeaders::Configuration.default do |config|
|
|
77
78
|
"storage",
|
78
79
|
"executionContexts"
|
79
80
|
]
|
81
|
+
config.expect_certificate_transparency = {
|
82
|
+
enforce: false,
|
83
|
+
max_age: 1.day.to_i,
|
84
|
+
report_uri: "https://report-uri.io/example-ct"
|
85
|
+
}
|
80
86
|
config.csp = {
|
81
87
|
# "meta" values. these will shape the header, but the values are not included in the header.
|
82
88
|
preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
|
@@ -94,6 +100,7 @@ SecureHeaders::Configuration.default do |config|
|
|
94
100
|
manifest_src: %w('self'),
|
95
101
|
media_src: %w(utoob.com),
|
96
102
|
object_src: %w('self'),
|
103
|
+
sandbox: true, # true and [] will set a maximally restrictive setting
|
97
104
|
plugin_types: %w(application/x-shockwave-flash),
|
98
105
|
script_src: %w('self'),
|
99
106
|
style_src: %w('unsafe-inline'),
|
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"
|
@@ -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,
|
@@ -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
|
@@ -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)
|
@@ -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?://}
|
@@ -264,7 +268,7 @@ module SecureHeaders
|
|
264
268
|
# when each hash contains a value for a given key.
|
265
269
|
def merge_policy_additions(original, additions)
|
266
270
|
original.merge(additions) do |directive, lhs, rhs|
|
267
|
-
if
|
271
|
+
if list_directive?(directive)
|
268
272
|
(lhs.to_a + rhs.to_a).compact.uniq
|
269
273
|
else
|
270
274
|
rhs
|
@@ -272,20 +276,27 @@ module SecureHeaders
|
|
272
276
|
end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
|
273
277
|
end
|
274
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
|
+
|
275
286
|
# For each directive in additions that does not exist in the original config,
|
276
287
|
# copy the default-src value to the original config. This modifies the original hash.
|
277
288
|
def populate_fetch_source_with_default!(original, additions)
|
278
289
|
# in case we would be appending to an empty directive, fill it with the default-src value
|
279
290
|
additions.each_key do |directive|
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
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)
|
289
300
|
end
|
290
301
|
end
|
291
302
|
end
|
@@ -296,45 +307,77 @@ module SecureHeaders
|
|
296
307
|
original[DEFAULT_SRC]
|
297
308
|
end
|
298
309
|
|
299
|
-
def nonce_added?(original, additions)
|
300
|
-
[:script_nonce, :style_nonce].each do |nonce|
|
301
|
-
if additions[nonce] && !original[nonce]
|
302
|
-
return true
|
303
|
-
end
|
304
|
-
end
|
305
|
-
end
|
306
|
-
|
307
310
|
def source_list?(directive)
|
308
311
|
DIRECTIVE_VALUE_TYPES[directive] == :source_list
|
309
312
|
end
|
310
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
|
+
|
311
322
|
# Private: Validates that the configuration has a valid type, or that it is a valid
|
312
323
|
# source expression.
|
313
|
-
def validate_directive!(directive,
|
324
|
+
def validate_directive!(directive, value)
|
325
|
+
ensure_valid_directive!(directive)
|
314
326
|
case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive]
|
315
327
|
when :boolean
|
316
|
-
unless boolean?(
|
317
|
-
raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean value")
|
318
|
-
end
|
319
|
-
when :string
|
320
|
-
unless source_expression.is_a?(String)
|
321
|
-
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")
|
322
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)
|
323
337
|
else
|
324
|
-
|
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)")
|
325
368
|
end
|
326
369
|
end
|
327
370
|
|
328
371
|
# Private: validates that a source expression:
|
329
|
-
# 1.
|
330
|
-
# 2.
|
331
|
-
# 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)
|
332
374
|
#
|
333
375
|
# Does not validate the invididual values of the source expression (e.g.
|
334
376
|
# script_src => h*t*t*p: will not raise an exception)
|
335
377
|
def validate_source_expression!(directive, source_expression)
|
336
|
-
|
337
|
-
|
378
|
+
if source_expression != OPT_OUT
|
379
|
+
ensure_array_of_strings!(directive, source_expression)
|
380
|
+
end
|
338
381
|
ensure_valid_sources!(directive, source_expression)
|
339
382
|
end
|
340
383
|
|
@@ -344,8 +387,8 @@ module SecureHeaders
|
|
344
387
|
end
|
345
388
|
end
|
346
389
|
|
347
|
-
def ensure_array_of_strings!(directive,
|
348
|
-
if (!
|
390
|
+
def ensure_array_of_strings!(directive, value)
|
391
|
+
if (!value.is_a?(Array) || !value.compact.all? { |v| v.is_a?(String) })
|
349
392
|
raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of strings")
|
350
393
|
end
|
351
394
|
end
|
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.alpha04"
|
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."
|
@@ -96,6 +96,21 @@ module SecureHeaders
|
|
96
96
|
expect(csp.value).to eq("default-src example.org")
|
97
97
|
end
|
98
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
|
+
|
99
114
|
it "does not emit a warning when using frame-src" do
|
100
115
|
expect(Kernel).to_not receive(:warn)
|
101
116
|
ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
|
@@ -120,50 +135,52 @@ module SecureHeaders
|
|
120
135
|
block_all_mixed_content: true,
|
121
136
|
upgrade_insecure_requests: true,
|
122
137
|
script_src: %w(script-src.com),
|
123
|
-
script_nonce: 123456
|
138
|
+
script_nonce: 123456,
|
139
|
+
sandbox: %w(allow-forms),
|
140
|
+
plugin_types: %w(application/pdf)
|
124
141
|
})
|
125
142
|
end
|
126
143
|
|
127
144
|
it "does not filter any directives for Chrome" do
|
128
145
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
|
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
|
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")
|
130
147
|
end
|
131
148
|
|
132
149
|
it "does not filter any directives for Opera" do
|
133
150
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
|
134
|
-
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")
|
135
152
|
end
|
136
153
|
|
137
154
|
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
|
138
155
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
|
139
|
-
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")
|
140
157
|
end
|
141
158
|
|
142
159
|
it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
|
143
160
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
|
144
|
-
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")
|
145
162
|
end
|
146
163
|
|
147
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
|
148
165
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
|
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
|
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")
|
150
167
|
end
|
151
168
|
|
152
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
|
153
170
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
|
154
|
-
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")
|
155
172
|
end
|
156
173
|
|
157
174
|
it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do
|
158
175
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10])
|
159
|
-
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")
|
160
177
|
end
|
161
178
|
|
162
179
|
it "falls back to standard Firefox defaults when the useragent version is not present" do
|
163
180
|
ua = USER_AGENTS[:firefox].dup
|
164
181
|
allow(ua).to receive(:version).and_return(nil)
|
165
182
|
policy = ContentSecurityPolicy.new(complex_opts, ua)
|
166
|
-
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")
|
167
184
|
end
|
168
185
|
end
|
169
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
|
@@ -111,6 +111,36 @@ module SecureHeaders
|
|
111
111
|
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval), script_src: %w('self')))
|
112
112
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
113
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
|
114
144
|
end
|
115
145
|
|
116
146
|
describe "#combine_policies" do
|
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.alpha04
|
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-05 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
|
@@ -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
|