secure_headers 6.3.4 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -2
- data/Gemfile +2 -0
- data/README.md +81 -15
- data/lib/secure_headers/configuration.rb +64 -13
- data/lib/secure_headers/headers/clear_site_data.rb +31 -33
- data/lib/secure_headers/headers/content_security_policy.rb +80 -46
- data/lib/secure_headers/headers/content_security_policy_config.rb +17 -58
- data/lib/secure_headers/headers/cookie.rb +4 -6
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +20 -22
- data/lib/secure_headers/headers/policy_management.rb +66 -18
- data/lib/secure_headers/headers/referrer_policy.rb +20 -22
- data/lib/secure_headers/headers/reporting_endpoints.rb +54 -0
- data/lib/secure_headers/headers/strict_transport_security.rb +13 -15
- data/lib/secure_headers/headers/x_content_type_options.rb +14 -16
- data/lib/secure_headers/headers/x_download_options.rb +14 -16
- data/lib/secure_headers/headers/x_frame_options.rb +14 -16
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +14 -16
- data/lib/secure_headers/headers/x_xss_protection.rb +14 -16
- data/lib/secure_headers/middleware.rb +11 -7
- data/lib/secure_headers/railtie.rb +11 -8
- data/lib/secure_headers/task_helper.rb +65 -0
- data/lib/secure_headers/version.rb +1 -1
- data/lib/secure_headers/view_helper.rb +7 -6
- data/lib/secure_headers.rb +26 -2
- data/lib/tasks/tasks.rake +4 -53
- data/secure_headers.gemspec +19 -7
- metadata +38 -70
- data/.github/ISSUE_TEMPLATE.md +0 -41
- data/.github/PULL_REQUEST_TEMPLATE.md +0 -20
- data/.github/workflows/build.yml +0 -24
- data/.github/workflows/github-release.yml +0 -28
- data/.gitignore +0 -13
- data/.rspec +0 -3
- data/.rubocop.yml +0 -4
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/CODE_OF_CONDUCT.md +0 -46
- data/CONTRIBUTING.md +0 -41
- data/Guardfile +0 -13
- data/Rakefile +0 -32
- data/docs/cookies.md +0 -65
- data/docs/hashes.md +0 -64
- data/docs/named_overrides_and_appends.md +0 -104
- data/docs/per_action_configuration.md +0 -139
- data/docs/sinatra.md +0 -25
- data/docs/upgrading-to-3-0.md +0 -42
- data/docs/upgrading-to-4-0.md +0 -35
- data/docs/upgrading-to-5-0.md +0 -15
- data/docs/upgrading-to-6-0.md +0 -50
- data/spec/lib/secure_headers/configuration_spec.rb +0 -121
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +0 -87
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +0 -190
- data/spec/lib/secure_headers/headers/cookie_spec.rb +0 -179
- data/spec/lib/secure_headers/headers/expect_certificate_transparency_spec.rb +0 -42
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +0 -264
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +0 -91
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +0 -33
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +0 -31
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +0 -29
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +0 -36
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +0 -48
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +0 -47
- data/spec/lib/secure_headers/middleware_spec.rb +0 -117
- data/spec/lib/secure_headers/view_helpers_spec.rb +0 -191
- data/spec/lib/secure_headers_spec.rb +0 -516
- data/spec/spec_helper.rb +0 -64
|
@@ -1,64 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
module SecureHeaders
|
|
3
3
|
module DynamicConfig
|
|
4
|
-
def self.included(base)
|
|
5
|
-
base.send(:attr_reader, *base.attrs)
|
|
6
|
-
base.attrs.each do |attr|
|
|
7
|
-
base.send(:define_method, "#{attr}=") do |value|
|
|
8
|
-
if self.class.attrs.include?(attr)
|
|
9
|
-
write_attribute(attr, value)
|
|
10
|
-
else
|
|
11
|
-
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}"
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
4
|
def initialize(hash)
|
|
18
|
-
@
|
|
19
|
-
@block_all_mixed_content = nil
|
|
20
|
-
@child_src = nil
|
|
21
|
-
@connect_src = nil
|
|
22
|
-
@default_src = nil
|
|
23
|
-
@font_src = nil
|
|
24
|
-
@form_action = nil
|
|
25
|
-
@frame_ancestors = nil
|
|
26
|
-
@frame_src = nil
|
|
27
|
-
@img_src = nil
|
|
28
|
-
@manifest_src = nil
|
|
29
|
-
@media_src = nil
|
|
30
|
-
@navigate_to = nil
|
|
31
|
-
@object_src = nil
|
|
32
|
-
@plugin_types = nil
|
|
33
|
-
@prefetch_src = nil
|
|
34
|
-
@preserve_schemes = nil
|
|
35
|
-
@report_only = nil
|
|
36
|
-
@report_uri = nil
|
|
37
|
-
@require_sri_for = nil
|
|
38
|
-
@sandbox = nil
|
|
39
|
-
@script_nonce = nil
|
|
40
|
-
@script_src = nil
|
|
41
|
-
@script_src_elem = nil
|
|
42
|
-
@script_src_attr = nil
|
|
43
|
-
@style_nonce = nil
|
|
44
|
-
@style_src = nil
|
|
45
|
-
@style_src_elem = nil
|
|
46
|
-
@style_src_attr = nil
|
|
47
|
-
@worker_src = nil
|
|
48
|
-
@upgrade_insecure_requests = nil
|
|
49
|
-
@disable_nonce_backwards_compatibility = nil
|
|
5
|
+
@config = {}
|
|
50
6
|
|
|
51
7
|
from_hash(hash)
|
|
52
8
|
end
|
|
53
9
|
|
|
10
|
+
def initialize_copy(hash)
|
|
11
|
+
@config = hash.to_h
|
|
12
|
+
end
|
|
13
|
+
|
|
54
14
|
def update_directive(directive, value)
|
|
55
|
-
|
|
15
|
+
@config[directive] = value
|
|
56
16
|
end
|
|
57
17
|
|
|
58
18
|
def directive_value(directive)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
end
|
|
19
|
+
# No need to check attrs, as we only assign valid keys
|
|
20
|
+
@config[directive]
|
|
62
21
|
end
|
|
63
22
|
|
|
64
23
|
def merge(new_hash)
|
|
@@ -76,10 +35,7 @@ module SecureHeaders
|
|
|
76
35
|
end
|
|
77
36
|
|
|
78
37
|
def to_h
|
|
79
|
-
|
|
80
|
-
value = self.send(key)
|
|
81
|
-
hash[key] = value unless value.nil?
|
|
82
|
-
end
|
|
38
|
+
@config.dup
|
|
83
39
|
end
|
|
84
40
|
|
|
85
41
|
def dup
|
|
@@ -112,16 +68,19 @@ module SecureHeaders
|
|
|
112
68
|
|
|
113
69
|
def write_attribute(attr, value)
|
|
114
70
|
value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
|
|
115
|
-
|
|
116
|
-
|
|
71
|
+
if value.nil?
|
|
72
|
+
@config.delete(attr)
|
|
73
|
+
else
|
|
74
|
+
@config[attr] = value
|
|
75
|
+
end
|
|
117
76
|
end
|
|
118
77
|
end
|
|
119
78
|
|
|
120
79
|
class ContentSecurityPolicyConfigError < StandardError; end
|
|
121
80
|
class ContentSecurityPolicyConfig
|
|
122
|
-
HEADER_NAME = "
|
|
81
|
+
HEADER_NAME = "content-security-policy".freeze
|
|
123
82
|
|
|
124
|
-
ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
|
|
83
|
+
ATTRS = Set.new(PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES)
|
|
125
84
|
def self.attrs
|
|
126
85
|
ATTRS
|
|
127
86
|
end
|
|
@@ -148,7 +107,7 @@ module SecureHeaders
|
|
|
148
107
|
end
|
|
149
108
|
|
|
150
109
|
class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig
|
|
151
|
-
HEADER_NAME = "
|
|
110
|
+
HEADER_NAME = "content-security-policy-report-only".freeze
|
|
152
111
|
|
|
153
112
|
def report_only?
|
|
154
113
|
true
|
|
@@ -7,10 +7,8 @@ module SecureHeaders
|
|
|
7
7
|
class CookiesConfigError < StandardError; end
|
|
8
8
|
class Cookie
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
CookiesConfig.new(config).validate!
|
|
13
|
-
end
|
|
10
|
+
def self.validate_config!(config)
|
|
11
|
+
CookiesConfig.new(config).validate!
|
|
14
12
|
end
|
|
15
13
|
|
|
16
14
|
attr_reader :raw_cookie, :config
|
|
@@ -80,9 +78,9 @@ module SecureHeaders
|
|
|
80
78
|
end
|
|
81
79
|
|
|
82
80
|
def conditionally_flag?(configuration)
|
|
83
|
-
if(Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?)
|
|
81
|
+
if (Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?)
|
|
84
82
|
true
|
|
85
|
-
elsif(Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?)
|
|
83
|
+
elsif (Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?)
|
|
86
84
|
true
|
|
87
85
|
else
|
|
88
86
|
false
|
|
@@ -3,37 +3,35 @@ module SecureHeaders
|
|
|
3
3
|
class ExpectCertificateTransparencyConfigError < StandardError; end
|
|
4
4
|
|
|
5
5
|
class ExpectCertificateTransparency
|
|
6
|
-
HEADER_NAME = "
|
|
6
|
+
HEADER_NAME = "expect-ct".freeze
|
|
7
7
|
INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze
|
|
8
8
|
INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze
|
|
9
9
|
REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze
|
|
10
10
|
INVALID_MAX_AGE_ERROR = "max-age must be a number.".freeze
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return if config.nil? || config == OPT_OUT
|
|
12
|
+
# Public: Generate a expect-ct header.
|
|
13
|
+
#
|
|
14
|
+
# Returns nil if not configured, returns header name and value if
|
|
15
|
+
# configured.
|
|
16
|
+
def self.make_header(config, use_agent = nil)
|
|
17
|
+
return if config.nil? || config == OPT_OUT
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
header = new(config)
|
|
20
|
+
[HEADER_NAME, header.value]
|
|
21
|
+
end
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
def self.validate_config!(config)
|
|
24
|
+
return if config.nil? || config == OPT_OUT
|
|
25
|
+
raise ExpectCertificateTransparencyConfigError.new(INVALID_CONFIGURATION_ERROR) unless config.is_a? Hash
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
unless [true, false, nil].include?(config[:enforce])
|
|
28
|
+
raise ExpectCertificateTransparencyConfigError.new(INVALID_ENFORCE_VALUE_ERROR)
|
|
29
|
+
end
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end
|
|
31
|
+
if !config[:max_age]
|
|
32
|
+
raise ExpectCertificateTransparencyConfigError.new(REQUIRED_MAX_AGE_ERROR)
|
|
33
|
+
elsif config[:max_age].to_s !~ /\A\d+\z/
|
|
34
|
+
raise ExpectCertificateTransparencyConfigError.new(INVALID_MAX_AGE_ERROR)
|
|
37
35
|
end
|
|
38
36
|
end
|
|
39
37
|
|
|
@@ -39,6 +39,7 @@ module SecureHeaders
|
|
|
39
39
|
SCRIPT_SRC = :script_src
|
|
40
40
|
STYLE_SRC = :style_src
|
|
41
41
|
REPORT_URI = :report_uri
|
|
42
|
+
REPORT_TO = :report_to
|
|
42
43
|
|
|
43
44
|
DIRECTIVES_1_0 = [
|
|
44
45
|
DEFAULT_SRC,
|
|
@@ -51,7 +52,8 @@ module SecureHeaders
|
|
|
51
52
|
SANDBOX,
|
|
52
53
|
SCRIPT_SRC,
|
|
53
54
|
STYLE_SRC,
|
|
54
|
-
REPORT_URI
|
|
55
|
+
REPORT_URI,
|
|
56
|
+
REPORT_TO
|
|
55
57
|
].freeze
|
|
56
58
|
|
|
57
59
|
BASE_URI = :base_uri
|
|
@@ -71,7 +73,6 @@ module SecureHeaders
|
|
|
71
73
|
|
|
72
74
|
# All the directives currently under consideration for CSP level 3.
|
|
73
75
|
# https://w3c.github.io/webappsec/specs/CSP2/
|
|
74
|
-
BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
|
|
75
76
|
MANIFEST_SRC = :manifest_src
|
|
76
77
|
NAVIGATE_TO = :navigate_to
|
|
77
78
|
PREFETCH_SRC = :prefetch_src
|
|
@@ -85,7 +86,6 @@ module SecureHeaders
|
|
|
85
86
|
|
|
86
87
|
DIRECTIVES_3_0 = [
|
|
87
88
|
DIRECTIVES_2_0,
|
|
88
|
-
BLOCK_ALL_MIXED_CONTENT,
|
|
89
89
|
MANIFEST_SRC,
|
|
90
90
|
NAVIGATE_TO,
|
|
91
91
|
PREFETCH_SRC,
|
|
@@ -98,15 +98,26 @@ module SecureHeaders
|
|
|
98
98
|
STYLE_SRC_ATTR
|
|
99
99
|
].flatten.freeze
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
# Experimental directives - these vary greatly in support
|
|
102
|
+
# See MDN for details.
|
|
103
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/content-security-policy/trusted-types
|
|
104
|
+
TRUSTED_TYPES = :trusted_types
|
|
105
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/content-security-policy/require-trusted-types-for
|
|
106
|
+
REQUIRE_TRUSTED_TYPES_FOR = :require_trusted_types_for
|
|
107
|
+
|
|
108
|
+
DIRECTIVES_EXPERIMENTAL = [
|
|
109
|
+
TRUSTED_TYPES,
|
|
110
|
+
REQUIRE_TRUSTED_TYPES_FOR,
|
|
111
|
+
].flatten.freeze
|
|
102
112
|
|
|
103
|
-
|
|
113
|
+
ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_EXPERIMENTAL).uniq.sort
|
|
114
|
+
|
|
115
|
+
# Think of default-src and report-uri/report-to as the beginning and end respectively,
|
|
104
116
|
# everything else is in between.
|
|
105
|
-
BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
|
|
117
|
+
BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI, REPORT_TO]
|
|
106
118
|
|
|
107
119
|
DIRECTIVE_VALUE_TYPES = {
|
|
108
120
|
BASE_URI => :source_list,
|
|
109
|
-
BLOCK_ALL_MIXED_CONTENT => :boolean,
|
|
110
121
|
CHILD_SRC => :source_list,
|
|
111
122
|
CONNECT_SRC => :source_list,
|
|
112
123
|
DEFAULT_SRC => :source_list,
|
|
@@ -120,9 +131,11 @@ module SecureHeaders
|
|
|
120
131
|
NAVIGATE_TO => :source_list,
|
|
121
132
|
OBJECT_SRC => :source_list,
|
|
122
133
|
PLUGIN_TYPES => :media_type_list,
|
|
123
|
-
REQUIRE_SRI_FOR => :require_sri_for_list,
|
|
124
|
-
REPORT_URI => :source_list,
|
|
125
134
|
PREFETCH_SRC => :source_list,
|
|
135
|
+
REPORT_TO => :report_to_endpoint,
|
|
136
|
+
REPORT_URI => :source_list,
|
|
137
|
+
REQUIRE_SRI_FOR => :require_sri_for_list,
|
|
138
|
+
REQUIRE_TRUSTED_TYPES_FOR => :require_trusted_types_for_list,
|
|
126
139
|
SANDBOX => :sandbox_list,
|
|
127
140
|
SCRIPT_SRC => :source_list,
|
|
128
141
|
SCRIPT_SRC_ELEM => :source_list,
|
|
@@ -130,6 +143,7 @@ module SecureHeaders
|
|
|
130
143
|
STYLE_SRC => :source_list,
|
|
131
144
|
STYLE_SRC_ELEM => :source_list,
|
|
132
145
|
STYLE_SRC_ATTR => :source_list,
|
|
146
|
+
TRUSTED_TYPES => :source_list,
|
|
133
147
|
WORKER_SRC => :source_list,
|
|
134
148
|
UPGRADE_INSECURE_REQUESTS => :boolean,
|
|
135
149
|
}.freeze
|
|
@@ -147,6 +161,7 @@ module SecureHeaders
|
|
|
147
161
|
FORM_ACTION,
|
|
148
162
|
FRAME_ANCESTORS,
|
|
149
163
|
NAVIGATE_TO,
|
|
164
|
+
REPORT_TO,
|
|
150
165
|
REPORT_URI,
|
|
151
166
|
]
|
|
152
167
|
|
|
@@ -175,6 +190,7 @@ module SecureHeaders
|
|
|
175
190
|
].freeze
|
|
176
191
|
|
|
177
192
|
REQUIRE_SRI_FOR_VALUES = Set.new(%w(script style))
|
|
193
|
+
REQUIRE_TRUSTED_TYPES_FOR_VALUES = Set.new(%w('script'))
|
|
178
194
|
|
|
179
195
|
module ClassMethods
|
|
180
196
|
# Public: generate a header name, value array that is user-agent-aware.
|
|
@@ -189,7 +205,7 @@ module SecureHeaders
|
|
|
189
205
|
|
|
190
206
|
# Public: Validates each source expression.
|
|
191
207
|
#
|
|
192
|
-
# Does not validate the
|
|
208
|
+
# Does not validate the individual values of the source expression (e.g.
|
|
193
209
|
# script_src => h*t*t*p: will not raise an exception)
|
|
194
210
|
def validate_config!(config)
|
|
195
211
|
return if config.nil? || config.opt_out?
|
|
@@ -226,7 +242,7 @@ module SecureHeaders
|
|
|
226
242
|
#
|
|
227
243
|
# raises an error if the original config is OPT_OUT
|
|
228
244
|
#
|
|
229
|
-
# 1. for non-source-list values (report_only,
|
|
245
|
+
# 1. for non-source-list values (report_only, upgrade_insecure_requests),
|
|
230
246
|
# additions will overwrite the original value.
|
|
231
247
|
# 2. if a value in additions does not exist in the original config, the
|
|
232
248
|
# default-src value is included to match original behavior.
|
|
@@ -270,7 +286,8 @@ module SecureHeaders
|
|
|
270
286
|
source_list?(directive) ||
|
|
271
287
|
sandbox_list?(directive) ||
|
|
272
288
|
media_type_list?(directive) ||
|
|
273
|
-
require_sri_for_list?(directive)
|
|
289
|
+
require_sri_for_list?(directive) ||
|
|
290
|
+
require_trusted_types_for_list?(directive)
|
|
274
291
|
end
|
|
275
292
|
|
|
276
293
|
# For each directive in additions that does not exist in the original config,
|
|
@@ -278,11 +295,12 @@ module SecureHeaders
|
|
|
278
295
|
def populate_fetch_source_with_default!(original, additions)
|
|
279
296
|
# in case we would be appending to an empty directive, fill it with the default-src value
|
|
280
297
|
additions.each_key do |directive|
|
|
281
|
-
directive =
|
|
282
|
-
directive.to_s.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
298
|
+
directive =
|
|
299
|
+
if directive.to_s.end_with?("_nonce")
|
|
300
|
+
directive.to_s.gsub(/_nonce/, "_src").to_sym
|
|
301
|
+
else
|
|
302
|
+
directive
|
|
303
|
+
end
|
|
286
304
|
# Don't set a default if directive has an existing value
|
|
287
305
|
next if original[directive]
|
|
288
306
|
if FETCH_SOURCES.include?(directive)
|
|
@@ -307,6 +325,10 @@ module SecureHeaders
|
|
|
307
325
|
DIRECTIVE_VALUE_TYPES[directive] == :require_sri_for_list
|
|
308
326
|
end
|
|
309
327
|
|
|
328
|
+
def require_trusted_types_for_list?(directive)
|
|
329
|
+
DIRECTIVE_VALUE_TYPES[directive] == :require_trusted_types_for_list
|
|
330
|
+
end
|
|
331
|
+
|
|
310
332
|
# Private: Validates that the configuration has a valid type, or that it is a valid
|
|
311
333
|
# source expression.
|
|
312
334
|
def validate_directive!(directive, value)
|
|
@@ -324,6 +346,10 @@ module SecureHeaders
|
|
|
324
346
|
validate_media_type_expression!(directive, value)
|
|
325
347
|
when :require_sri_for_list
|
|
326
348
|
validate_require_sri_source_expression!(directive, value)
|
|
349
|
+
when :require_trusted_types_for_list
|
|
350
|
+
validate_require_trusted_types_for_source_expression!(directive, value)
|
|
351
|
+
when :report_to_endpoint
|
|
352
|
+
validate_report_to_endpoint_expression!(directive, value)
|
|
327
353
|
else
|
|
328
354
|
raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
|
|
329
355
|
end
|
|
@@ -368,11 +394,33 @@ module SecureHeaders
|
|
|
368
394
|
end
|
|
369
395
|
end
|
|
370
396
|
|
|
397
|
+
# Private: validates that a require trusted types for expression:
|
|
398
|
+
# 1. is an array of strings
|
|
399
|
+
# 2. is a subset of ["'script'"]
|
|
400
|
+
def validate_require_trusted_types_for_source_expression!(directive, require_trusted_types_for_expression)
|
|
401
|
+
ensure_array_of_strings!(directive, require_trusted_types_for_expression)
|
|
402
|
+
unless require_trusted_types_for_expression.to_set.subset?(REQUIRE_TRUSTED_TYPES_FOR_VALUES)
|
|
403
|
+
raise ContentSecurityPolicyConfigError.new(%(require-trusted-types-for for must be a subset of #{REQUIRE_TRUSTED_TYPES_FOR_VALUES.to_a} but was #{require_trusted_types_for_expression}))
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Private: validates that a report-to endpoint expression:
|
|
408
|
+
# 1. is a string
|
|
409
|
+
# 2. is not empty
|
|
410
|
+
def validate_report_to_endpoint_expression!(directive, endpoint_name)
|
|
411
|
+
unless endpoint_name.is_a?(String)
|
|
412
|
+
raise ContentSecurityPolicyConfigError.new("#{directive} must be a string. Found #{endpoint_name.class} value")
|
|
413
|
+
end
|
|
414
|
+
if endpoint_name.empty?
|
|
415
|
+
raise ContentSecurityPolicyConfigError.new("#{directive} must not be empty")
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
371
419
|
# Private: validates that a source expression:
|
|
372
420
|
# 1. is an array of strings
|
|
373
421
|
# 2. does not contain any deprecated, now invalid values (inline, eval, self, none)
|
|
374
422
|
#
|
|
375
|
-
# Does not validate the
|
|
423
|
+
# Does not validate the individual values of the source expression (e.g.
|
|
376
424
|
# script_src => h*t*t*p: will not raise an exception)
|
|
377
425
|
def validate_source_expression!(directive, source_expression)
|
|
378
426
|
if source_expression != OPT_OUT
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
module SecureHeaders
|
|
3
3
|
class ReferrerPolicyConfigError < StandardError; end
|
|
4
4
|
class ReferrerPolicy
|
|
5
|
-
HEADER_NAME = "
|
|
5
|
+
HEADER_NAME = "referrer-policy".freeze
|
|
6
6
|
DEFAULT_VALUE = "origin-when-cross-origin"
|
|
7
7
|
VALID_POLICIES = %w(
|
|
8
8
|
no-referrer
|
|
@@ -15,29 +15,27 @@ module SecureHeaders
|
|
|
15
15
|
unsafe-url
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
end
|
|
18
|
+
# Public: generate an Referrer Policy header.
|
|
19
|
+
#
|
|
20
|
+
# Returns a default header if no configuration is provided, or a
|
|
21
|
+
# header name and value based on the config.
|
|
22
|
+
def self.make_header(config = nil, user_agent = nil)
|
|
23
|
+
return if config == OPT_OUT
|
|
24
|
+
config ||= DEFAULT_VALUE
|
|
25
|
+
[HEADER_NAME, Array(config).join(", ")]
|
|
26
|
+
end
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
end
|
|
38
|
-
else
|
|
39
|
-
raise TypeError.new("Must be a string or array of strings. Found #{config.class}: #{config}")
|
|
28
|
+
def self.validate_config!(config)
|
|
29
|
+
case config
|
|
30
|
+
when nil, OPT_OUT
|
|
31
|
+
# valid
|
|
32
|
+
when String, Array
|
|
33
|
+
config = Array(config)
|
|
34
|
+
unless config.all? { |t| t.is_a?(String) && VALID_POLICIES.include?(t.downcase) }
|
|
35
|
+
raise ReferrerPolicyConfigError.new("Value can only be one or more of #{VALID_POLICIES.join(", ")}")
|
|
40
36
|
end
|
|
37
|
+
else
|
|
38
|
+
raise TypeError.new("Must be a string or array of strings. Found #{config.class}: #{config}")
|
|
41
39
|
end
|
|
42
40
|
end
|
|
43
41
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module SecureHeaders
|
|
3
|
+
class ReportingEndpointsConfigError < StandardError; end
|
|
4
|
+
class ReportingEndpoints
|
|
5
|
+
HEADER_NAME = "reporting-endpoints".freeze
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
# Public: generate a Reporting-Endpoints header.
|
|
9
|
+
#
|
|
10
|
+
# The config should be a Hash of endpoint names to URLs.
|
|
11
|
+
# Example: { "csp-endpoint" => "https://example.com/reports" }
|
|
12
|
+
#
|
|
13
|
+
# Returns nil if config is OPT_OUT or nil, or a header name and
|
|
14
|
+
# formatted header value based on the config.
|
|
15
|
+
def make_header(config = nil)
|
|
16
|
+
return if config.nil? || config == OPT_OUT
|
|
17
|
+
validate_config!(config)
|
|
18
|
+
[HEADER_NAME, format_endpoints(config)]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def validate_config!(config)
|
|
22
|
+
case config
|
|
23
|
+
when nil, OPT_OUT
|
|
24
|
+
# valid
|
|
25
|
+
when Hash
|
|
26
|
+
config.each_pair do |name, url|
|
|
27
|
+
if name.is_a?(Symbol)
|
|
28
|
+
name = name.to_s
|
|
29
|
+
end
|
|
30
|
+
unless name.is_a?(String) && !name.empty?
|
|
31
|
+
raise ReportingEndpointsConfigError.new("Endpoint name must be a non-empty string, got: #{name.inspect}")
|
|
32
|
+
end
|
|
33
|
+
unless url.is_a?(String) && !url.empty?
|
|
34
|
+
raise ReportingEndpointsConfigError.new("Endpoint URL must be a non-empty string, got: #{url.inspect}")
|
|
35
|
+
end
|
|
36
|
+
unless url.start_with?("https://")
|
|
37
|
+
raise ReportingEndpointsConfigError.new("Endpoint URLs must use https, got: #{url.inspect}")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
else
|
|
41
|
+
raise TypeError.new("Must be a Hash of endpoint names to URLs. Found #{config.class}: #{config}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def format_endpoints(config)
|
|
48
|
+
config.map do |name, url|
|
|
49
|
+
%{#{name}="#{url}"}
|
|
50
|
+
end.join(", ")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -3,27 +3,25 @@ module SecureHeaders
|
|
|
3
3
|
class STSConfigError < StandardError; end
|
|
4
4
|
|
|
5
5
|
class StrictTransportSecurity
|
|
6
|
-
HEADER_NAME = "
|
|
6
|
+
HEADER_NAME = "strict-transport-security".freeze
|
|
7
7
|
HSTS_MAX_AGE = "631138519"
|
|
8
8
|
DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE
|
|
9
9
|
VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i
|
|
10
10
|
MESSAGE = "The config value supplied for the HSTS header was invalid. Must match #{VALID_STS_HEADER}"
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
end
|
|
12
|
+
# Public: generate an hsts header name, value pair.
|
|
13
|
+
#
|
|
14
|
+
# Returns a default header if no configuration is provided, or a
|
|
15
|
+
# header name and value based on the config.
|
|
16
|
+
def self.make_header(config = nil, user_agent = nil)
|
|
17
|
+
return if config == OPT_OUT
|
|
18
|
+
[HEADER_NAME, config || DEFAULT_VALUE]
|
|
19
|
+
end
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
21
|
+
def self.validate_config!(config)
|
|
22
|
+
return if config.nil? || config == OPT_OUT
|
|
23
|
+
raise TypeError.new("Must be a string. Found #{config.class}: #{config} #{config.class}") unless config.is_a?(String)
|
|
24
|
+
raise STSConfigError.new(MESSAGE) unless config =~ VALID_STS_HEADER
|
|
27
25
|
end
|
|
28
26
|
end
|
|
29
27
|
end
|
|
@@ -3,25 +3,23 @@ module SecureHeaders
|
|
|
3
3
|
class XContentTypeOptionsConfigError < StandardError; end
|
|
4
4
|
|
|
5
5
|
class XContentTypeOptions
|
|
6
|
-
HEADER_NAME = "
|
|
6
|
+
HEADER_NAME = "x-content-type-options".freeze
|
|
7
7
|
DEFAULT_VALUE = "nosniff"
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
end
|
|
9
|
+
# Public: generate an X-Content-Type-Options header.
|
|
10
|
+
#
|
|
11
|
+
# Returns a default header if no configuration is provided, or a
|
|
12
|
+
# header name and value based on the config.
|
|
13
|
+
def self.make_header(config = nil, user_agent = nil)
|
|
14
|
+
return if config == OPT_OUT
|
|
15
|
+
[HEADER_NAME, config || DEFAULT_VALUE]
|
|
16
|
+
end
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
end
|
|
18
|
+
def self.validate_config!(config)
|
|
19
|
+
return if config.nil? || config == OPT_OUT
|
|
20
|
+
raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String)
|
|
21
|
+
unless config.casecmp(DEFAULT_VALUE) == 0
|
|
22
|
+
raise XContentTypeOptionsConfigError.new("Value can only be nil or 'nosniff'")
|
|
25
23
|
end
|
|
26
24
|
end
|
|
27
25
|
end
|
|
@@ -2,25 +2,23 @@
|
|
|
2
2
|
module SecureHeaders
|
|
3
3
|
class XDOConfigError < StandardError; end
|
|
4
4
|
class XDownloadOptions
|
|
5
|
-
HEADER_NAME = "
|
|
5
|
+
HEADER_NAME = "x-download-options".freeze
|
|
6
6
|
DEFAULT_VALUE = "noopen"
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
end
|
|
8
|
+
# Public: generate an x-download-options header.
|
|
9
|
+
#
|
|
10
|
+
# Returns a default header if no configuration is provided, or a
|
|
11
|
+
# header name and value based on the config.
|
|
12
|
+
def self.make_header(config = nil, user_agent = nil)
|
|
13
|
+
return if config == OPT_OUT
|
|
14
|
+
[HEADER_NAME, config || DEFAULT_VALUE]
|
|
15
|
+
end
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
end
|
|
17
|
+
def self.validate_config!(config)
|
|
18
|
+
return if config.nil? || config == OPT_OUT
|
|
19
|
+
raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String)
|
|
20
|
+
unless config.casecmp(DEFAULT_VALUE) == 0
|
|
21
|
+
raise XDOConfigError.new("Value can only be nil or 'noopen'")
|
|
24
22
|
end
|
|
25
23
|
end
|
|
26
24
|
end
|