secure_headers 3.6.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rspec +2 -0
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -1
- data/.travis.yml +8 -4
- data/CHANGELOG.md +38 -1
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +7 -4
- data/Guardfile +1 -0
- data/LICENSE +4 -199
- data/README.md +18 -20
- data/Rakefile +22 -18
- data/docs/cookies.md +16 -2
- data/docs/hashes.md +1 -1
- data/docs/per_action_configuration.md +28 -0
- data/lib/secure_headers/configuration.rb +30 -17
- data/lib/secure_headers/hash_helper.rb +2 -1
- data/lib/secure_headers/headers/clear_site_data.rb +16 -12
- data/lib/secure_headers/headers/content_security_policy.rb +54 -22
- data/lib/secure_headers/headers/content_security_policy_config.rb +46 -12
- data/lib/secure_headers/headers/cookie.rb +21 -3
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
- data/lib/secure_headers/headers/policy_management.rb +122 -67
- data/lib/secure_headers/headers/public_key_pins.rb +5 -4
- data/lib/secure_headers/headers/referrer_policy.rb +2 -1
- data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
- data/lib/secure_headers/headers/x_content_type_options.rb +2 -1
- data/lib/secure_headers/headers/x_download_options.rb +3 -2
- data/lib/secure_headers/headers/x_frame_options.rb +2 -1
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
- data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
- data/lib/secure_headers/middleware.rb +10 -9
- data/lib/secure_headers/railtie.rb +7 -6
- data/lib/secure_headers/utils/cookies_config.rb +13 -12
- data/lib/secure_headers/view_helper.rb +4 -3
- data/lib/secure_headers.rb +7 -3
- data/lib/tasks/tasks.rake +2 -1
- data/secure_headers.gemspec +13 -3
- data/spec/lib/secure_headers/configuration_spec.rb +9 -8
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +11 -27
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +47 -26
- data/spec/lib/secure_headers/headers/cookie_spec.rb +38 -20
- data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +57 -10
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +4 -3
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +5 -4
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
- data/spec/lib/secure_headers/middleware_spec.rb +29 -21
- data/spec/lib/secure_headers/view_helpers_spec.rb +5 -5
- data/spec/lib/secure_headers_spec.rb +106 -107
- data/spec/spec_helper.rb +10 -22
- data/upgrading-to-4-0.md +55 -0
- metadata +19 -7
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "yaml"
|
|
2
3
|
|
|
3
4
|
module SecureHeaders
|
|
4
5
|
class Configuration
|
|
@@ -31,7 +32,7 @@ module SecureHeaders
|
|
|
31
32
|
raise NotYetConfiguredError, "#{base} policy not yet supplied"
|
|
32
33
|
end
|
|
33
34
|
override = @configurations[base].dup
|
|
34
|
-
override.instance_eval
|
|
35
|
+
override.instance_eval(&block) if block_given?
|
|
35
36
|
add_configuration(name, override)
|
|
36
37
|
end
|
|
37
38
|
|
|
@@ -116,23 +117,41 @@ module SecureHeaders
|
|
|
116
117
|
|
|
117
118
|
attr_writer :hsts, :x_frame_options, :x_content_type_options,
|
|
118
119
|
:x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
|
|
119
|
-
:referrer_policy, :clear_site_data
|
|
120
|
+
:referrer_policy, :clear_site_data, :expect_certificate_transparency
|
|
120
121
|
|
|
121
122
|
attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
|
|
122
123
|
|
|
124
|
+
@script_hashes = nil
|
|
125
|
+
@style_hashes = nil
|
|
126
|
+
|
|
123
127
|
HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
|
|
124
|
-
if File.
|
|
128
|
+
if File.exist?(HASH_CONFIG_FILE)
|
|
125
129
|
config = YAML.safe_load(File.open(HASH_CONFIG_FILE))
|
|
126
130
|
@script_hashes = config["scripts"]
|
|
127
131
|
@style_hashes = config["styles"]
|
|
128
132
|
end
|
|
129
133
|
|
|
130
134
|
def initialize(&block)
|
|
135
|
+
@cookies = nil
|
|
136
|
+
@clear_site_data = nil
|
|
137
|
+
@csp = nil
|
|
138
|
+
@csp_report_only = nil
|
|
139
|
+
@hpkp_report_host = nil
|
|
140
|
+
@hpkp = nil
|
|
141
|
+
@hsts = nil
|
|
142
|
+
@x_content_type_options = nil
|
|
143
|
+
@x_download_options = nil
|
|
144
|
+
@x_frame_options = nil
|
|
145
|
+
@x_permitted_cross_domain_policies = nil
|
|
146
|
+
@x_xss_protection = nil
|
|
147
|
+
@expect_certificate_transparency = nil
|
|
148
|
+
|
|
131
149
|
self.hpkp = OPT_OUT
|
|
132
150
|
self.referrer_policy = OPT_OUT
|
|
133
151
|
self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT)
|
|
134
152
|
self.csp_report_only = OPT_OUT
|
|
135
|
-
|
|
153
|
+
|
|
154
|
+
instance_eval(&block) if block_given?
|
|
136
155
|
end
|
|
137
156
|
|
|
138
157
|
# Public: copy everything but the cached headers
|
|
@@ -151,6 +170,7 @@ module SecureHeaders
|
|
|
151
170
|
copy.x_download_options = @x_download_options
|
|
152
171
|
copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
|
|
153
172
|
copy.clear_site_data = @clear_site_data
|
|
173
|
+
copy.expect_certificate_transparency = @expect_certificate_transparency
|
|
154
174
|
copy.referrer_policy = @referrer_policy
|
|
155
175
|
copy.hpkp = @hpkp
|
|
156
176
|
copy.hpkp_report_host = @hpkp_report_host
|
|
@@ -183,13 +203,13 @@ module SecureHeaders
|
|
|
183
203
|
XDownloadOptions.validate_config!(@x_download_options)
|
|
184
204
|
XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
|
|
185
205
|
ClearSiteData.validate_config!(@clear_site_data)
|
|
206
|
+
ExpectCertificateTransparency.validate_config!(@expect_certificate_transparency)
|
|
186
207
|
PublicKeyPins.validate_config!(@hpkp)
|
|
187
208
|
Cookie.validate_config!(@cookies)
|
|
188
209
|
end
|
|
189
210
|
|
|
190
211
|
def secure_cookies=(secure_cookies)
|
|
191
|
-
|
|
192
|
-
@cookies = (@cookies || {}).merge(secure: secure_cookies)
|
|
212
|
+
raise ArgumentError, "#{Kernel.caller.first}: `#secure_cookies=` is no longer supported. Please use `#cookies=` to configure secure cookies instead."
|
|
193
213
|
end
|
|
194
214
|
|
|
195
215
|
def csp=(new_csp)
|
|
@@ -197,10 +217,8 @@ module SecureHeaders
|
|
|
197
217
|
@csp = new_csp.dup
|
|
198
218
|
else
|
|
199
219
|
if new_csp[:report_only]
|
|
200
|
-
#
|
|
201
|
-
|
|
202
|
-
@csp = OPT_OUT
|
|
203
|
-
self.csp_report_only = new_csp
|
|
220
|
+
# invalid configuration implies that CSPRO should be set, CSP should not - so opt out
|
|
221
|
+
raise ArgumentError, "#{Kernel.caller.first}: `#csp=` was supplied a config with report_only: true. Use #csp_report_only="
|
|
204
222
|
else
|
|
205
223
|
@csp = ContentSecurityPolicyConfig.new(new_csp)
|
|
206
224
|
end
|
|
@@ -227,11 +245,6 @@ module SecureHeaders
|
|
|
227
245
|
end
|
|
228
246
|
end
|
|
229
247
|
end
|
|
230
|
-
|
|
231
|
-
if !@csp_report_only.opt_out? && @csp.to_h == ContentSecurityPolicyConfig::DEFAULT
|
|
232
|
-
Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp_report_only=` was configured before `#csp=`. It is assumed you intended to opt out of `#csp=` so be sure to add `config.csp = SecureHeaders::OPT_OUT` to your config. Ensure that #csp_report_only is configured after #csp="
|
|
233
|
-
@csp = OPT_OUT
|
|
234
|
-
end
|
|
235
248
|
end
|
|
236
249
|
|
|
237
250
|
protected
|
|
@@ -262,7 +275,7 @@ module SecureHeaders
|
|
|
262
275
|
end
|
|
263
276
|
end
|
|
264
277
|
|
|
265
|
-
# Public: Precompute the header names and values for this
|
|
278
|
+
# Public: Precompute the header names and values for this configuration.
|
|
266
279
|
# Ensures that headers generated at configure time, not on demand.
|
|
267
280
|
#
|
|
268
281
|
# Returns the cached headers
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module SecureHeaders
|
|
2
3
|
class ClearSiteDataConfigError < StandardError; end
|
|
3
4
|
class ClearSiteData
|
|
4
5
|
HEADER_NAME = "Clear-Site-Data".freeze
|
|
5
|
-
TYPES = "types".freeze
|
|
6
6
|
|
|
7
7
|
# Valid `types`
|
|
8
8
|
CACHE = "cache".freeze
|
|
9
9
|
COOKIES = "cookies".freeze
|
|
10
10
|
STORAGE = "storage".freeze
|
|
11
|
-
|
|
12
|
-
ALL_TYPES = [CACHE, COOKIES, STORAGE,
|
|
11
|
+
EXECUTION_CONTEXTS = "executionContexts".freeze
|
|
12
|
+
ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECUTION_CONTEXTS]
|
|
13
13
|
|
|
14
14
|
CONFIG_KEY = :clear_site_data
|
|
15
15
|
|
|
@@ -17,14 +17,14 @@ module SecureHeaders
|
|
|
17
17
|
# Public: make an Clear-Site-Data header name, value pair
|
|
18
18
|
#
|
|
19
19
|
# Returns nil if not configured, returns header name and value if configured.
|
|
20
|
-
def make_header(config=nil)
|
|
20
|
+
def make_header(config = nil)
|
|
21
21
|
case config
|
|
22
22
|
when nil, OPT_OUT, []
|
|
23
23
|
# noop
|
|
24
24
|
when Array
|
|
25
|
-
[HEADER_NAME,
|
|
25
|
+
[HEADER_NAME, make_header_value(config)]
|
|
26
26
|
when true
|
|
27
|
-
[HEADER_NAME,
|
|
27
|
+
[HEADER_NAME, make_header_value(ALL_TYPES)]
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -36,16 +36,20 @@ module SecureHeaders
|
|
|
36
36
|
unless config.all? { |t| t.is_a?(String) }
|
|
37
37
|
raise ClearSiteDataConfigError.new("types must be Strings")
|
|
38
38
|
end
|
|
39
|
-
|
|
40
|
-
begin
|
|
41
|
-
JSON.dump(config)
|
|
42
|
-
rescue JSON::GeneratorError, Encoding::UndefinedConversionError
|
|
43
|
-
raise ClearSiteDataConfigError.new("types must serializable by JSON")
|
|
44
|
-
end
|
|
45
39
|
else
|
|
46
40
|
raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`")
|
|
47
41
|
end
|
|
48
42
|
end
|
|
43
|
+
|
|
44
|
+
# Public: Transform a Clear-Site-Data config (an Array of Strings) into a
|
|
45
|
+
# String that can be used as the value for the Clear-Site-Data header.
|
|
46
|
+
#
|
|
47
|
+
# types - An Array of String of types of data to clear.
|
|
48
|
+
#
|
|
49
|
+
# Returns a String of quoted values that are comma separated.
|
|
50
|
+
def make_header_value(types)
|
|
51
|
+
types.map { |t| "\"#{t}\""}.join(", ")
|
|
52
|
+
end
|
|
49
53
|
end
|
|
50
54
|
end
|
|
51
55
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
require_relative
|
|
3
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "policy_management"
|
|
3
|
+
require_relative "content_security_policy_config"
|
|
4
|
+
require "useragent"
|
|
4
5
|
|
|
5
6
|
module SecureHeaders
|
|
6
7
|
class ContentSecurityPolicy
|
|
@@ -54,20 +55,12 @@ module SecureHeaders
|
|
|
54
55
|
|
|
55
56
|
private
|
|
56
57
|
|
|
57
|
-
# frame-src is deprecated, child-src is being implemented. They are
|
|
58
|
-
# very similar and in most cases, the same value can be used for both.
|
|
59
58
|
def normalize_child_frame_src
|
|
60
59
|
if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src
|
|
61
|
-
|
|
62
|
-
elsif @config.frame_src
|
|
63
|
-
Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config.frame_src}.")
|
|
60
|
+
raise ArgumentError, "#{Kernel.caller.first}: both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers."
|
|
64
61
|
end
|
|
65
62
|
|
|
66
|
-
|
|
67
|
-
@config.child_src || @config.frame_src
|
|
68
|
-
else
|
|
69
|
-
@config.frame_src || @config.child_src
|
|
70
|
-
end
|
|
63
|
+
@config.frame_src || @config.child_src
|
|
71
64
|
end
|
|
72
65
|
|
|
73
66
|
# Private: converts the config object into a string representing a policy.
|
|
@@ -82,20 +75,55 @@ module SecureHeaders
|
|
|
82
75
|
case DIRECTIVE_VALUE_TYPES[directive_name]
|
|
83
76
|
when :boolean
|
|
84
77
|
symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
|
|
85
|
-
when :
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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)
|
|
89
84
|
end
|
|
90
85
|
end.compact.join("; ")
|
|
91
86
|
end
|
|
92
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
|
+
|
|
93
121
|
# Private: builds a string that represents one directive in a minified form.
|
|
94
122
|
#
|
|
95
123
|
# directive_name - a symbol representing the various ALL_DIRECTIVES
|
|
96
124
|
#
|
|
97
125
|
# Returns a string representing a directive.
|
|
98
|
-
def
|
|
126
|
+
def build_source_list_directive(directive)
|
|
99
127
|
source_list = case directive
|
|
100
128
|
when :child_src
|
|
101
129
|
if supported_directives.include?(:child_src)
|
|
@@ -108,9 +136,11 @@ module SecureHeaders
|
|
|
108
136
|
else
|
|
109
137
|
@config.directive_value(directive)
|
|
110
138
|
end
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
139
|
+
|
|
140
|
+
if source_list != OPT_OUT && source_list && source_list.any?
|
|
141
|
+
normalized_source_list = minify_source_list(directive, source_list)
|
|
142
|
+
[symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
|
|
143
|
+
end
|
|
114
144
|
end
|
|
115
145
|
|
|
116
146
|
# If a directive contains *, all other values are omitted.
|
|
@@ -216,6 +246,8 @@ module SecureHeaders
|
|
|
216
246
|
@supported_directives ||= if VARIATIONS[@parsed_ua.browser]
|
|
217
247
|
if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
|
|
218
248
|
VARIATIONS["FirefoxTransitional"]
|
|
249
|
+
elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10)
|
|
250
|
+
VARIATIONS["SafariTransitional"]
|
|
219
251
|
else
|
|
220
252
|
VARIATIONS[@parsed_ua.browser]
|
|
221
253
|
end
|
|
@@ -229,7 +261,7 @@ module SecureHeaders
|
|
|
229
261
|
end
|
|
230
262
|
|
|
231
263
|
def symbol_to_hyphen_case(sym)
|
|
232
|
-
sym.to_s.tr(
|
|
264
|
+
sym.to_s.tr("_", "-")
|
|
233
265
|
end
|
|
234
266
|
end
|
|
235
267
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module SecureHeaders
|
|
2
3
|
module DynamicConfig
|
|
3
4
|
def self.included(base)
|
|
@@ -6,12 +7,7 @@ module SecureHeaders
|
|
|
6
7
|
base.attrs.each do |attr|
|
|
7
8
|
base.send(:define_method, "#{attr}=") do |value|
|
|
8
9
|
if self.class.attrs.include?(attr)
|
|
9
|
-
|
|
10
|
-
prev_value = self.instance_variable_get("@#{attr}")
|
|
11
|
-
self.instance_variable_set("@#{attr}", value)
|
|
12
|
-
if prev_value != self.instance_variable_get("@#{attr}")
|
|
13
|
-
@modified = true
|
|
14
|
-
end
|
|
10
|
+
write_attribute(attr, value)
|
|
15
11
|
else
|
|
16
12
|
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}"
|
|
17
13
|
end
|
|
@@ -20,6 +16,30 @@ module SecureHeaders
|
|
|
20
16
|
end
|
|
21
17
|
|
|
22
18
|
def initialize(hash)
|
|
19
|
+
@base_uri = nil
|
|
20
|
+
@block_all_mixed_content = nil
|
|
21
|
+
@child_src = nil
|
|
22
|
+
@connect_src = nil
|
|
23
|
+
@default_src = nil
|
|
24
|
+
@font_src = nil
|
|
25
|
+
@form_action = nil
|
|
26
|
+
@frame_ancestors = nil
|
|
27
|
+
@frame_src = nil
|
|
28
|
+
@img_src = nil
|
|
29
|
+
@manifest_src = nil
|
|
30
|
+
@media_src = nil
|
|
31
|
+
@object_src = nil
|
|
32
|
+
@plugin_types = nil
|
|
33
|
+
@preserve_schemes = nil
|
|
34
|
+
@report_only = nil
|
|
35
|
+
@report_uri = nil
|
|
36
|
+
@sandbox = nil
|
|
37
|
+
@script_nonce = nil
|
|
38
|
+
@script_src = nil
|
|
39
|
+
@style_nonce = nil
|
|
40
|
+
@style_src = nil
|
|
41
|
+
@upgrade_insecure_requests = nil
|
|
42
|
+
|
|
23
43
|
from_hash(hash)
|
|
24
44
|
@modified = false
|
|
25
45
|
end
|
|
@@ -52,8 +72,9 @@ module SecureHeaders
|
|
|
52
72
|
|
|
53
73
|
def to_h
|
|
54
74
|
self.class.attrs.each_with_object({}) do |key, hash|
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
value = self.send(key)
|
|
76
|
+
hash[key] = value unless value.nil?
|
|
77
|
+
end
|
|
57
78
|
end
|
|
58
79
|
|
|
59
80
|
def dup
|
|
@@ -73,14 +94,26 @@ module SecureHeaders
|
|
|
73
94
|
|
|
74
95
|
private
|
|
75
96
|
def from_hash(hash)
|
|
76
|
-
hash.
|
|
97
|
+
hash.each_pair do |k, v|
|
|
98
|
+
next if v.nil?
|
|
99
|
+
|
|
77
100
|
if self.class.attrs.include?(k)
|
|
78
|
-
|
|
101
|
+
write_attribute(k, v)
|
|
79
102
|
else
|
|
80
|
-
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{
|
|
103
|
+
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{v}"
|
|
81
104
|
end
|
|
82
105
|
end
|
|
83
106
|
end
|
|
107
|
+
|
|
108
|
+
def write_attribute(attr, value)
|
|
109
|
+
value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
|
|
110
|
+
attr_variable = "@#{attr}"
|
|
111
|
+
prev_value = self.instance_variable_get(attr_variable)
|
|
112
|
+
self.instance_variable_set(attr_variable, value)
|
|
113
|
+
if prev_value != value
|
|
114
|
+
@modified = true
|
|
115
|
+
end
|
|
116
|
+
end
|
|
84
117
|
end
|
|
85
118
|
|
|
86
119
|
class ContentSecurityPolicyConfigError < StandardError; end
|
|
@@ -88,8 +121,9 @@ module SecureHeaders
|
|
|
88
121
|
CONFIG_KEY = :csp
|
|
89
122
|
HEADER_NAME = "Content-Security-Policy".freeze
|
|
90
123
|
|
|
124
|
+
ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
|
|
91
125
|
def self.attrs
|
|
92
|
-
|
|
126
|
+
ATTRS
|
|
93
127
|
end
|
|
94
128
|
|
|
95
129
|
include DynamicConfig
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
require
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "cgi"
|
|
3
|
+
require "secure_headers/utils/cookies_config"
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
module SecureHeaders
|
|
5
7
|
class CookiesConfigError < StandardError; end
|
|
@@ -13,8 +15,18 @@ module SecureHeaders
|
|
|
13
15
|
|
|
14
16
|
attr_reader :raw_cookie, :config
|
|
15
17
|
|
|
18
|
+
COOKIE_DEFAULTS = {
|
|
19
|
+
httponly: true,
|
|
20
|
+
secure: true,
|
|
21
|
+
samesite: { lax: true },
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
16
24
|
def initialize(cookie, config)
|
|
17
25
|
@raw_cookie = cookie
|
|
26
|
+
unless config == OPT_OUT
|
|
27
|
+
config ||= {}
|
|
28
|
+
config = COOKIE_DEFAULTS.merge(config)
|
|
29
|
+
end
|
|
18
30
|
@config = config
|
|
19
31
|
@attributes = {
|
|
20
32
|
httponly: nil,
|
|
@@ -56,6 +68,7 @@ module SecureHeaders
|
|
|
56
68
|
end
|
|
57
69
|
|
|
58
70
|
def flag_cookie?(attribute)
|
|
71
|
+
return false if config == OPT_OUT
|
|
59
72
|
case config[attribute]
|
|
60
73
|
when TrueClass
|
|
61
74
|
true
|
|
@@ -85,6 +98,7 @@ module SecureHeaders
|
|
|
85
98
|
end
|
|
86
99
|
|
|
87
100
|
def flag_samesite?
|
|
101
|
+
return false if config == OPT_OUT || config[:samesite] == OPT_OUT
|
|
88
102
|
flag_samesite_lax? || flag_samesite_strict?
|
|
89
103
|
end
|
|
90
104
|
|
|
@@ -99,6 +113,10 @@ module SecureHeaders
|
|
|
99
113
|
def flag_samesite_enforcement?(mode)
|
|
100
114
|
return unless config[:samesite]
|
|
101
115
|
|
|
116
|
+
if config[:samesite].is_a?(TrueClass) && mode == :lax
|
|
117
|
+
return true
|
|
118
|
+
end
|
|
119
|
+
|
|
102
120
|
case config[:samesite][mode]
|
|
103
121
|
when Hash
|
|
104
122
|
conditionally_flag?(config[:samesite][mode])
|
|
@@ -113,7 +131,7 @@ module SecureHeaders
|
|
|
113
131
|
return unless cookie
|
|
114
132
|
|
|
115
133
|
cookie.split(/[;,]\s?/).each do |pairs|
|
|
116
|
-
name, values = pairs.split(
|
|
134
|
+
name, values = pairs.split("=", 2)
|
|
117
135
|
name = CGI.unescape(name)
|
|
118
136
|
|
|
119
137
|
attribute = name.downcase.to_sym
|
|
@@ -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
|