secure_headers 3.4.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 +113 -1
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +8 -4
- data/Guardfile +1 -0
- data/LICENSE +4 -199
- data/README.md +63 -333
- data/Rakefile +22 -18
- data/docs/HPKP.md +17 -0
- data/docs/cookies.md +64 -0
- data/docs/hashes.md +64 -0
- data/docs/named_overrides_and_appends.md +107 -0
- data/docs/per_action_configuration.md +133 -0
- data/docs/sinatra.md +25 -0
- data/lib/secure_headers/configuration.rb +91 -44
- data/lib/secure_headers/hash_helper.rb +2 -1
- data/lib/secure_headers/headers/clear_site_data.rb +55 -0
- data/lib/secure_headers/headers/content_security_policy.rb +110 -51
- data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
- 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 +143 -69
- data/lib/secure_headers/headers/public_key_pins.rb +5 -4
- data/lib/secure_headers/headers/referrer_policy.rb +5 -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 +12 -3
- data/lib/secure_headers.rb +137 -55
- data/lib/tasks/tasks.rake +2 -1
- data/secure_headers.gemspec +13 -3
- data/spec/lib/secure_headers/configuration_spec.rb +13 -12
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +59 -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 +78 -40
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +22 -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 +39 -19
- data/spec/lib/secure_headers/view_helpers_spec.rb +21 -8
- data/spec/lib/secure_headers_spec.rb +304 -56
- data/spec/spec_helper.rb +13 -24
- data/upgrading-to-4-0.md +55 -0
- metadata +29 -7
|
@@ -1,37 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "policy_management"
|
|
3
|
+
require_relative "content_security_policy_config"
|
|
4
|
+
require "useragent"
|
|
3
5
|
|
|
4
6
|
module SecureHeaders
|
|
5
|
-
class ContentSecurityPolicyConfigError < StandardError; end
|
|
6
7
|
class ContentSecurityPolicy
|
|
7
8
|
include PolicyManagement
|
|
8
9
|
|
|
9
10
|
# constants to be used for version-specific UA sniffing
|
|
10
11
|
VERSION_46 = ::UserAgent::Version.new("46")
|
|
12
|
+
VERSION_10 = ::UserAgent::Version.new("10")
|
|
13
|
+
FALLBACK_VERSION = ::UserAgent::Version.new("0")
|
|
11
14
|
|
|
12
15
|
def initialize(config = nil, user_agent = OTHER)
|
|
13
|
-
@config =
|
|
16
|
+
@config = if config.is_a?(Hash)
|
|
17
|
+
if config[:report_only]
|
|
18
|
+
ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
|
|
19
|
+
else
|
|
20
|
+
ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG)
|
|
21
|
+
end
|
|
22
|
+
elsif config.nil?
|
|
23
|
+
ContentSecurityPolicyConfig.new(DEFAULT_CONFIG)
|
|
24
|
+
else
|
|
25
|
+
config
|
|
26
|
+
end
|
|
27
|
+
|
|
14
28
|
@parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
|
|
15
29
|
user_agent
|
|
16
30
|
else
|
|
17
31
|
UserAgent.parse(user_agent)
|
|
18
32
|
end
|
|
19
|
-
normalize_child_frame_src
|
|
20
|
-
@
|
|
21
|
-
@
|
|
22
|
-
@
|
|
23
|
-
@style_nonce = @config[:style_nonce]
|
|
33
|
+
@frame_src = normalize_child_frame_src
|
|
34
|
+
@preserve_schemes = @config.preserve_schemes
|
|
35
|
+
@script_nonce = @config.script_nonce
|
|
36
|
+
@style_nonce = @config.style_nonce
|
|
24
37
|
end
|
|
25
38
|
|
|
26
39
|
##
|
|
27
40
|
# Returns the name to use for the header. Either "Content-Security-Policy" or
|
|
28
41
|
# "Content-Security-Policy-Report-Only"
|
|
29
42
|
def name
|
|
30
|
-
|
|
31
|
-
REPORT_ONLY
|
|
32
|
-
else
|
|
33
|
-
HEADER_NAME
|
|
34
|
-
end
|
|
43
|
+
@config.class.const_get(:HEADER_NAME)
|
|
35
44
|
end
|
|
36
45
|
|
|
37
46
|
##
|
|
@@ -46,20 +55,12 @@ module SecureHeaders
|
|
|
46
55
|
|
|
47
56
|
private
|
|
48
57
|
|
|
49
|
-
# frame-src is deprecated, child-src is being implemented. They are
|
|
50
|
-
# very similar and in most cases, the same value can be used for both.
|
|
51
58
|
def normalize_child_frame_src
|
|
52
|
-
if @config
|
|
53
|
-
|
|
54
|
-
elsif @config[:frame_src]
|
|
55
|
-
Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config[:frame_src]}.")
|
|
59
|
+
if @config.frame_src && @config.child_src && @config.frame_src != @config.child_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."
|
|
56
61
|
end
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
@config[:child_src] = @config[:child_src] || @config[:frame_src]
|
|
60
|
-
else
|
|
61
|
-
@config[:frame_src] = @config[:frame_src] || @config[:child_src]
|
|
62
|
-
end
|
|
63
|
+
@config.frame_src || @config.child_src
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
# Private: converts the config object into a string representing a policy.
|
|
@@ -73,44 +74,90 @@ module SecureHeaders
|
|
|
73
74
|
directives.map do |directive_name|
|
|
74
75
|
case DIRECTIVE_VALUE_TYPES[directive_name]
|
|
75
76
|
when :boolean
|
|
76
|
-
symbol_to_hyphen_case(directive_name) if @config
|
|
77
|
-
when :
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
|
|
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)
|
|
81
84
|
end
|
|
82
85
|
end.compact.join("; ")
|
|
83
86
|
end
|
|
84
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
|
+
|
|
85
121
|
# Private: builds a string that represents one directive in a minified form.
|
|
86
122
|
#
|
|
87
123
|
# directive_name - a symbol representing the various ALL_DIRECTIVES
|
|
88
124
|
#
|
|
89
125
|
# Returns a string representing a directive.
|
|
90
|
-
def
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
126
|
+
def build_source_list_directive(directive)
|
|
127
|
+
source_list = case directive
|
|
128
|
+
when :child_src
|
|
129
|
+
if supported_directives.include?(:child_src)
|
|
130
|
+
@frame_src
|
|
131
|
+
end
|
|
132
|
+
when :frame_src
|
|
133
|
+
unless supported_directives.include?(:child_src)
|
|
134
|
+
@frame_src
|
|
135
|
+
end
|
|
136
|
+
else
|
|
137
|
+
@config.directive_value(directive)
|
|
138
|
+
end
|
|
95
139
|
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
98
144
|
end
|
|
99
145
|
|
|
100
146
|
# If a directive contains *, all other values are omitted.
|
|
101
147
|
# If a directive contains 'none' but has other values, 'none' is ommitted.
|
|
102
148
|
# Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
|
|
103
149
|
def minify_source_list(directive, source_list)
|
|
150
|
+
source_list = source_list.compact
|
|
104
151
|
if source_list.include?(STAR)
|
|
105
152
|
keep_wildcard_sources(source_list)
|
|
106
153
|
else
|
|
107
|
-
populate_nonces
|
|
108
|
-
reject_all_values_if_none
|
|
154
|
+
source_list = populate_nonces(directive, source_list)
|
|
155
|
+
source_list = reject_all_values_if_none(source_list)
|
|
109
156
|
|
|
110
157
|
unless directive == REPORT_URI || @preserve_schemes
|
|
111
|
-
strip_source_schemes
|
|
158
|
+
source_list = strip_source_schemes(source_list)
|
|
112
159
|
end
|
|
113
|
-
dedup_source_list(source_list)
|
|
160
|
+
dedup_source_list(source_list)
|
|
114
161
|
end
|
|
115
162
|
end
|
|
116
163
|
|
|
@@ -120,8 +167,12 @@ module SecureHeaders
|
|
|
120
167
|
end
|
|
121
168
|
|
|
122
169
|
# Discard any 'none' values if more directives are supplied since none may override values.
|
|
123
|
-
def reject_all_values_if_none
|
|
124
|
-
|
|
170
|
+
def reject_all_values_if_none(source_list)
|
|
171
|
+
if source_list.length > 1
|
|
172
|
+
source_list.reject { |value| value == NONE }
|
|
173
|
+
else
|
|
174
|
+
source_list
|
|
175
|
+
end
|
|
125
176
|
end
|
|
126
177
|
|
|
127
178
|
# Removes duplicates and sources that already match an existing wild card.
|
|
@@ -143,12 +194,14 @@ module SecureHeaders
|
|
|
143
194
|
|
|
144
195
|
# Private: append a nonce to the script/style directories if script_nonce
|
|
145
196
|
# or style_nonce are provided.
|
|
146
|
-
def populate_nonces
|
|
197
|
+
def populate_nonces(directive, source_list)
|
|
147
198
|
case directive
|
|
148
199
|
when SCRIPT_SRC
|
|
149
200
|
append_nonce(source_list, @script_nonce)
|
|
150
201
|
when STYLE_SRC
|
|
151
202
|
append_nonce(source_list, @style_nonce)
|
|
203
|
+
else
|
|
204
|
+
source_list
|
|
152
205
|
end
|
|
153
206
|
end
|
|
154
207
|
|
|
@@ -165,19 +218,23 @@ module SecureHeaders
|
|
|
165
218
|
source_list << UNSAFE_INLINE
|
|
166
219
|
end
|
|
167
220
|
end
|
|
221
|
+
|
|
222
|
+
source_list
|
|
168
223
|
end
|
|
169
224
|
|
|
170
225
|
# Private: return the list of directives that are supported by the user agent,
|
|
171
226
|
# starting with default-src and ending with report-uri.
|
|
172
227
|
def directives
|
|
173
|
-
[
|
|
228
|
+
[
|
|
229
|
+
DEFAULT_SRC,
|
|
174
230
|
BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
|
|
175
|
-
REPORT_URI
|
|
231
|
+
REPORT_URI
|
|
232
|
+
].flatten
|
|
176
233
|
end
|
|
177
234
|
|
|
178
235
|
# Private: Remove scheme from source expressions.
|
|
179
|
-
def strip_source_schemes
|
|
180
|
-
source_list.map
|
|
236
|
+
def strip_source_schemes(source_list)
|
|
237
|
+
source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
|
|
181
238
|
end
|
|
182
239
|
|
|
183
240
|
# Private: determine which directives are supported for the given user agent.
|
|
@@ -187,8 +244,10 @@ module SecureHeaders
|
|
|
187
244
|
# Returns an array of symbols representing the directives.
|
|
188
245
|
def supported_directives
|
|
189
246
|
@supported_directives ||= if VARIATIONS[@parsed_ua.browser]
|
|
190
|
-
if @parsed_ua.browser == "Firefox" && @parsed_ua.version >= VERSION_46
|
|
247
|
+
if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
|
|
191
248
|
VARIATIONS["FirefoxTransitional"]
|
|
249
|
+
elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10)
|
|
250
|
+
VARIATIONS["SafariTransitional"]
|
|
192
251
|
else
|
|
193
252
|
VARIATIONS[@parsed_ua.browser]
|
|
194
253
|
end
|
|
@@ -198,11 +257,11 @@ module SecureHeaders
|
|
|
198
257
|
end
|
|
199
258
|
|
|
200
259
|
def nonces_supported?
|
|
201
|
-
@nonces_supported ||=
|
|
260
|
+
@nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
|
|
202
261
|
end
|
|
203
262
|
|
|
204
263
|
def symbol_to_hyphen_case(sym)
|
|
205
|
-
sym.to_s.tr(
|
|
264
|
+
sym.to_s.tr("_", "-")
|
|
206
265
|
end
|
|
207
266
|
end
|
|
208
267
|
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module SecureHeaders
|
|
3
|
+
module DynamicConfig
|
|
4
|
+
def self.included(base)
|
|
5
|
+
base.send(:attr_writer, :modified)
|
|
6
|
+
base.send(:attr_reader, *base.attrs)
|
|
7
|
+
base.attrs.each do |attr|
|
|
8
|
+
base.send(:define_method, "#{attr}=") do |value|
|
|
9
|
+
if self.class.attrs.include?(attr)
|
|
10
|
+
write_attribute(attr, value)
|
|
11
|
+
else
|
|
12
|
+
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
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
|
+
|
|
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
|
|
@@ -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
|