secure_headers 3.2.0 → 3.8.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 +5 -5
- data/.github/ISSUE_TEMPLATE.md +41 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +2 -0
- data/CHANGELOG.md +160 -1
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +3 -1
- data/LICENSE +4 -199
- data/README.md +75 -334
- data/docs/HPKP.md +17 -0
- data/docs/cookies.md +51 -0
- data/docs/hashes.md +64 -0
- data/docs/named_overrides_and_appends.md +107 -0
- data/docs/per_action_configuration.md +105 -0
- data/docs/sinatra.md +25 -0
- data/lib/secure_headers/configuration.rb +120 -39
- data/lib/secure_headers/headers/clear_site_data.rb +54 -0
- data/lib/secure_headers/headers/content_security_policy.rb +134 -36
- data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
- data/lib/secure_headers/headers/cookie.rb +7 -1
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
- data/lib/secure_headers/headers/policy_management.rb +150 -66
- data/lib/secure_headers/headers/public_key_pins.rb +1 -1
- data/lib/secure_headers/headers/referrer_policy.rb +36 -0
- data/lib/secure_headers/headers/strict_transport_security.rb +1 -1
- data/lib/secure_headers/headers/x_content_type_options.rb +1 -1
- data/lib/secure_headers/headers/x_download_options.rb +1 -1
- data/lib/secure_headers/headers/x_frame_options.rb +1 -1
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +1 -1
- data/lib/secure_headers/headers/x_xss_protection.rb +1 -1
- data/lib/secure_headers/middleware.rb +6 -0
- data/lib/secure_headers/railtie.rb +1 -1
- data/lib/secure_headers/utils/cookies_config.rb +6 -4
- data/lib/secure_headers/view_helper.rb +13 -7
- data/lib/secure_headers.rb +138 -55
- data/lib/tasks/tasks.rake +5 -4
- data/secure_headers.gemspec +2 -2
- data/spec/lib/secure_headers/configuration_spec.rb +5 -5
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +86 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +96 -13
- data/spec/lib/secure_headers/headers/cookie_spec.rb +22 -25
- data/spec/lib/secure_headers/headers/expect_certificate_transparency_spec.rb +42 -0
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +59 -36
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +72 -0
- data/spec/lib/secure_headers/middleware_spec.rb +31 -1
- data/spec/lib/secure_headers/view_helpers_spec.rb +19 -7
- data/spec/lib/secure_headers_spec.rb +344 -44
- data/spec/spec_helper.rb +8 -3
- data/upgrading-to-3-0.md +13 -10
- metadata +24 -5
|
@@ -1,33 +1,45 @@
|
|
|
1
1
|
require_relative 'policy_management'
|
|
2
|
+
require_relative 'content_security_policy_config'
|
|
3
|
+
require 'useragent'
|
|
2
4
|
|
|
3
5
|
module SecureHeaders
|
|
4
|
-
class ContentSecurityPolicyConfigError < StandardError; end
|
|
5
6
|
class ContentSecurityPolicy
|
|
6
7
|
include PolicyManagement
|
|
7
8
|
|
|
9
|
+
# constants to be used for version-specific UA sniffing
|
|
10
|
+
VERSION_46 = ::UserAgent::Version.new("46")
|
|
11
|
+
VERSION_10 = ::UserAgent::Version.new("10")
|
|
12
|
+
FALLBACK_VERSION = ::UserAgent::Version.new("0")
|
|
13
|
+
|
|
8
14
|
def initialize(config = nil, user_agent = OTHER)
|
|
9
|
-
config =
|
|
10
|
-
|
|
15
|
+
@config = if config.is_a?(Hash)
|
|
16
|
+
if config[:report_only]
|
|
17
|
+
ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
|
|
18
|
+
else
|
|
19
|
+
ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG)
|
|
20
|
+
end
|
|
21
|
+
elsif config.nil?
|
|
22
|
+
ContentSecurityPolicyConfig.new(DEFAULT_CONFIG)
|
|
23
|
+
else
|
|
24
|
+
config
|
|
25
|
+
end
|
|
26
|
+
|
|
11
27
|
@parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
|
|
12
28
|
user_agent
|
|
13
29
|
else
|
|
14
30
|
UserAgent.parse(user_agent)
|
|
15
31
|
end
|
|
16
|
-
@
|
|
17
|
-
@preserve_schemes = @config
|
|
18
|
-
@script_nonce = @config
|
|
19
|
-
@style_nonce = @config
|
|
32
|
+
@frame_src = normalize_child_frame_src
|
|
33
|
+
@preserve_schemes = @config.preserve_schemes
|
|
34
|
+
@script_nonce = @config.script_nonce
|
|
35
|
+
@style_nonce = @config.style_nonce
|
|
20
36
|
end
|
|
21
37
|
|
|
22
38
|
##
|
|
23
39
|
# Returns the name to use for the header. Either "Content-Security-Policy" or
|
|
24
40
|
# "Content-Security-Policy-Report-Only"
|
|
25
41
|
def name
|
|
26
|
-
|
|
27
|
-
REPORT_ONLY
|
|
28
|
-
else
|
|
29
|
-
HEADER_NAME
|
|
30
|
-
end
|
|
42
|
+
@config.class.const_get(:HEADER_NAME)
|
|
31
43
|
end
|
|
32
44
|
|
|
33
45
|
##
|
|
@@ -42,6 +54,20 @@ module SecureHeaders
|
|
|
42
54
|
|
|
43
55
|
private
|
|
44
56
|
|
|
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
|
+
def normalize_child_frame_src
|
|
60
|
+
if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src
|
|
61
|
+
Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers.")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if supported_directives.include?(:child_src)
|
|
65
|
+
@config.child_src || @config.frame_src
|
|
66
|
+
else
|
|
67
|
+
@config.frame_src || @config.child_src
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
45
71
|
# Private: converts the config object into a string representing a policy.
|
|
46
72
|
# Places default-src at the first directive and report-uri as the last. All
|
|
47
73
|
# others are presented in alphabetical order.
|
|
@@ -53,44 +79,94 @@ module SecureHeaders
|
|
|
53
79
|
directives.map do |directive_name|
|
|
54
80
|
case DIRECTIVE_VALUE_TYPES[directive_name]
|
|
55
81
|
when :boolean
|
|
56
|
-
symbol_to_hyphen_case(directive_name)
|
|
57
|
-
when :
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
82
|
+
symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
|
|
83
|
+
when :sandbox_list
|
|
84
|
+
build_sandbox_list_directive(directive_name)
|
|
85
|
+
when :media_type_list
|
|
86
|
+
build_media_type_list_directive(directive_name)
|
|
87
|
+
when :source_list
|
|
88
|
+
build_source_list_directive(directive_name)
|
|
61
89
|
end
|
|
62
90
|
end.compact.join("; ")
|
|
63
91
|
end
|
|
64
92
|
|
|
93
|
+
def build_sandbox_list_directive(directive)
|
|
94
|
+
return unless sandbox_list = @config.directive_value(directive)
|
|
95
|
+
max_strict_policy = case sandbox_list
|
|
96
|
+
when Array
|
|
97
|
+
sandbox_list.empty?
|
|
98
|
+
when true
|
|
99
|
+
true
|
|
100
|
+
else
|
|
101
|
+
false
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# A maximally strict sandbox policy is just the `sandbox` directive,
|
|
105
|
+
# whith no configuraiton values.
|
|
106
|
+
if max_strict_policy
|
|
107
|
+
symbol_to_hyphen_case(directive)
|
|
108
|
+
elsif sandbox_list && sandbox_list.any?
|
|
109
|
+
[
|
|
110
|
+
symbol_to_hyphen_case(directive),
|
|
111
|
+
sandbox_list.uniq
|
|
112
|
+
].join(" ")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def build_media_type_list_directive(directive)
|
|
117
|
+
return unless media_type_list = @config.directive_value(directive)
|
|
118
|
+
if media_type_list && media_type_list.any?
|
|
119
|
+
[
|
|
120
|
+
symbol_to_hyphen_case(directive),
|
|
121
|
+
media_type_list.uniq
|
|
122
|
+
].join(" ")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
65
126
|
# Private: builds a string that represents one directive in a minified form.
|
|
66
127
|
#
|
|
67
128
|
# directive_name - a symbol representing the various ALL_DIRECTIVES
|
|
68
129
|
#
|
|
69
130
|
# Returns a string representing a directive.
|
|
70
|
-
def
|
|
71
|
-
|
|
131
|
+
def build_source_list_directive(directive)
|
|
132
|
+
source_list = case directive
|
|
133
|
+
when :child_src
|
|
134
|
+
if supported_directives.include?(:child_src)
|
|
135
|
+
@frame_src
|
|
136
|
+
end
|
|
137
|
+
when :frame_src
|
|
138
|
+
unless supported_directives.include?(:child_src)
|
|
139
|
+
@frame_src
|
|
140
|
+
end
|
|
141
|
+
else
|
|
142
|
+
@config.directive_value(directive)
|
|
143
|
+
end
|
|
144
|
+
return unless source_list && source_list.any?
|
|
145
|
+
normalized_source_list = minify_source_list(directive, source_list).join(" ")
|
|
72
146
|
|
|
73
|
-
|
|
74
|
-
|
|
147
|
+
if normalized_source_list.include?(";")
|
|
148
|
+
Kernel.warn("#{directive} contains a ; in '#{normalized_source_list}' which will raise an error in future versions. It has been replaced with a blank space.")
|
|
149
|
+
end
|
|
150
|
+
escaped_source_list = normalized_source_list.gsub(";", " ")
|
|
75
151
|
|
|
76
|
-
|
|
77
|
-
[symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
|
|
152
|
+
[symbol_to_hyphen_case(directive), escaped_source_list].join(" ").strip
|
|
78
153
|
end
|
|
79
154
|
|
|
80
155
|
# If a directive contains *, all other values are omitted.
|
|
81
156
|
# If a directive contains 'none' but has other values, 'none' is ommitted.
|
|
82
157
|
# Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
|
|
83
158
|
def minify_source_list(directive, source_list)
|
|
159
|
+
source_list = source_list.compact
|
|
84
160
|
if source_list.include?(STAR)
|
|
85
161
|
keep_wildcard_sources(source_list)
|
|
86
162
|
else
|
|
87
|
-
populate_nonces
|
|
88
|
-
reject_all_values_if_none
|
|
163
|
+
source_list = populate_nonces(directive, source_list)
|
|
164
|
+
source_list = reject_all_values_if_none(source_list)
|
|
89
165
|
|
|
90
166
|
unless directive == REPORT_URI || @preserve_schemes
|
|
91
|
-
strip_source_schemes
|
|
167
|
+
source_list = strip_source_schemes(source_list)
|
|
92
168
|
end
|
|
93
|
-
dedup_source_list(source_list)
|
|
169
|
+
dedup_source_list(source_list)
|
|
94
170
|
end
|
|
95
171
|
end
|
|
96
172
|
|
|
@@ -100,8 +176,12 @@ module SecureHeaders
|
|
|
100
176
|
end
|
|
101
177
|
|
|
102
178
|
# Discard any 'none' values if more directives are supplied since none may override values.
|
|
103
|
-
def reject_all_values_if_none
|
|
104
|
-
|
|
179
|
+
def reject_all_values_if_none(source_list)
|
|
180
|
+
if source_list.length > 1
|
|
181
|
+
source_list.reject { |value| value == NONE }
|
|
182
|
+
else
|
|
183
|
+
source_list
|
|
184
|
+
end
|
|
105
185
|
end
|
|
106
186
|
|
|
107
187
|
# Removes duplicates and sources that already match an existing wild card.
|
|
@@ -123,12 +203,14 @@ module SecureHeaders
|
|
|
123
203
|
|
|
124
204
|
# Private: append a nonce to the script/style directories if script_nonce
|
|
125
205
|
# or style_nonce are provided.
|
|
126
|
-
def populate_nonces
|
|
206
|
+
def populate_nonces(directive, source_list)
|
|
127
207
|
case directive
|
|
128
208
|
when SCRIPT_SRC
|
|
129
209
|
append_nonce(source_list, @script_nonce)
|
|
130
210
|
when STYLE_SRC
|
|
131
211
|
append_nonce(source_list, @style_nonce)
|
|
212
|
+
else
|
|
213
|
+
source_list
|
|
132
214
|
end
|
|
133
215
|
end
|
|
134
216
|
|
|
@@ -145,30 +227,46 @@ module SecureHeaders
|
|
|
145
227
|
source_list << UNSAFE_INLINE
|
|
146
228
|
end
|
|
147
229
|
end
|
|
230
|
+
|
|
231
|
+
source_list
|
|
148
232
|
end
|
|
149
233
|
|
|
150
234
|
# Private: return the list of directives that are supported by the user agent,
|
|
151
235
|
# starting with default-src and ending with report-uri.
|
|
152
236
|
def directives
|
|
153
|
-
[
|
|
237
|
+
[
|
|
238
|
+
DEFAULT_SRC,
|
|
154
239
|
BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
|
|
155
|
-
REPORT_URI
|
|
240
|
+
REPORT_URI
|
|
241
|
+
].flatten
|
|
156
242
|
end
|
|
157
243
|
|
|
158
244
|
# Private: Remove scheme from source expressions.
|
|
159
|
-
def strip_source_schemes
|
|
160
|
-
source_list.map
|
|
245
|
+
def strip_source_schemes(source_list)
|
|
246
|
+
source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
|
|
161
247
|
end
|
|
162
248
|
|
|
163
249
|
# Private: determine which directives are supported for the given user agent.
|
|
164
250
|
#
|
|
251
|
+
# Add UA-sniffing special casing here.
|
|
252
|
+
#
|
|
165
253
|
# Returns an array of symbols representing the directives.
|
|
166
254
|
def supported_directives
|
|
167
|
-
@supported_directives ||= VARIATIONS[@parsed_ua.browser]
|
|
255
|
+
@supported_directives ||= if VARIATIONS[@parsed_ua.browser]
|
|
256
|
+
if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
|
|
257
|
+
VARIATIONS["FirefoxTransitional"]
|
|
258
|
+
elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10)
|
|
259
|
+
VARIATIONS["SafariTransitional"]
|
|
260
|
+
else
|
|
261
|
+
VARIATIONS[@parsed_ua.browser]
|
|
262
|
+
end
|
|
263
|
+
else
|
|
264
|
+
VARIATIONS[OTHER]
|
|
265
|
+
end
|
|
168
266
|
end
|
|
169
267
|
|
|
170
268
|
def nonces_supported?
|
|
171
|
-
@nonces_supported ||=
|
|
269
|
+
@nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
|
|
172
270
|
end
|
|
173
271
|
|
|
174
272
|
def symbol_to_hyphen_case(sym)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
module DynamicConfig
|
|
3
|
+
def self.included(base)
|
|
4
|
+
base.send(:attr_writer, :modified)
|
|
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
|
+
def initialize(hash)
|
|
18
|
+
@base_uri = nil
|
|
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
|
+
@object_src = nil
|
|
31
|
+
@plugin_types = nil
|
|
32
|
+
@preserve_schemes = nil
|
|
33
|
+
@report_only = nil
|
|
34
|
+
@report_uri = nil
|
|
35
|
+
@sandbox = nil
|
|
36
|
+
@script_nonce = nil
|
|
37
|
+
@script_src = nil
|
|
38
|
+
@style_nonce = nil
|
|
39
|
+
@style_src = nil
|
|
40
|
+
@worker_src = nil
|
|
41
|
+
@upgrade_insecure_requests = nil
|
|
42
|
+
|
|
43
|
+
from_hash(hash)
|
|
44
|
+
@modified = false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def update_directive(directive, value)
|
|
48
|
+
self.send("#{directive}=", value)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def directive_value(directive)
|
|
52
|
+
if self.class.attrs.include?(directive)
|
|
53
|
+
self.send(directive)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def modified?
|
|
58
|
+
@modified
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def merge(new_hash)
|
|
62
|
+
ContentSecurityPolicy.combine_policies(self.to_h, new_hash)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def merge!(new_hash)
|
|
66
|
+
from_hash(new_hash)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def append(new_hash)
|
|
70
|
+
from_hash(ContentSecurityPolicy.combine_policies(self.to_h, new_hash))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_h
|
|
74
|
+
self.class.attrs.each_with_object({}) do |key, hash|
|
|
75
|
+
value = self.send(key)
|
|
76
|
+
hash[key] = value unless value.nil?
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def dup
|
|
81
|
+
self.class.new(self.to_h)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def opt_out?
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def ==(o)
|
|
89
|
+
self.class == o.class && self.to_h == o.to_h
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
alias_method :[], :directive_value
|
|
93
|
+
alias_method :[]=, :update_directive
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
def from_hash(hash)
|
|
97
|
+
hash.each_pair do |k, v|
|
|
98
|
+
next if v.nil?
|
|
99
|
+
|
|
100
|
+
if self.class.attrs.include?(k)
|
|
101
|
+
write_attribute(k, v)
|
|
102
|
+
else
|
|
103
|
+
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{v}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
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
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class ContentSecurityPolicyConfigError < StandardError; end
|
|
120
|
+
class ContentSecurityPolicyConfig
|
|
121
|
+
CONFIG_KEY = :csp
|
|
122
|
+
HEADER_NAME = "Content-Security-Policy".freeze
|
|
123
|
+
|
|
124
|
+
ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
|
|
125
|
+
def self.attrs
|
|
126
|
+
ATTRS
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
include DynamicConfig
|
|
130
|
+
|
|
131
|
+
# based on what was suggested in https://github.com/rails/rails/pull/24961/files
|
|
132
|
+
DEFAULT = {
|
|
133
|
+
default_src: %w('self' https:),
|
|
134
|
+
font_src: %w('self' https: data:),
|
|
135
|
+
img_src: %w('self' https: data:),
|
|
136
|
+
object_src: %w('none'),
|
|
137
|
+
script_src: %w(https:),
|
|
138
|
+
style_src: %w('self' https: 'unsafe-inline')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
def report_only?
|
|
142
|
+
false
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def make_report_only
|
|
146
|
+
ContentSecurityPolicyReportOnlyConfig.new(self.to_h)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig
|
|
151
|
+
CONFIG_KEY = :csp_report_only
|
|
152
|
+
HEADER_NAME = "Content-Security-Policy-Report-Only".freeze
|
|
153
|
+
|
|
154
|
+
def report_only?
|
|
155
|
+
true
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def make_report_only
|
|
159
|
+
self
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -81,11 +81,13 @@ module SecureHeaders
|
|
|
81
81
|
"SameSite=Lax"
|
|
82
82
|
elsif flag_samesite_strict?
|
|
83
83
|
"SameSite=Strict"
|
|
84
|
+
elsif flag_samesite_none?
|
|
85
|
+
"SameSite=None"
|
|
84
86
|
end
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
def flag_samesite?
|
|
88
|
-
flag_samesite_lax? || flag_samesite_strict?
|
|
90
|
+
flag_samesite_lax? || flag_samesite_strict? || flag_samesite_none?
|
|
89
91
|
end
|
|
90
92
|
|
|
91
93
|
def flag_samesite_lax?
|
|
@@ -96,6 +98,10 @@ module SecureHeaders
|
|
|
96
98
|
flag_samesite_enforcement?(:strict)
|
|
97
99
|
end
|
|
98
100
|
|
|
101
|
+
def flag_samesite_none?
|
|
102
|
+
flag_samesite_enforcement?(:none)
|
|
103
|
+
end
|
|
104
|
+
|
|
99
105
|
def flag_samesite_enforcement?(mode)
|
|
100
106
|
return unless config[:samesite]
|
|
101
107
|
|
|
@@ -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
|
+
header_value = [
|
|
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
|