secure_headers 4.0.0.alpha03 → 4.0.0.alpha04
Sign up to get free protection for your applications and to get access to all the features.
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
|