secure_headers 3.2.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/.github/ISSUE_TEMPLATE.md +41 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- data/.rspec +2 -0
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -1
- data/.travis.yml +8 -4
- data/CHANGELOG.md +144 -1
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +9 -4
- data/Guardfile +1 -0
- data/LICENSE +4 -199
- data/README.md +73 -344
- 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 +116 -42
- 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 +128 -39
- 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 +158 -68
- data/lib/secure_headers/headers/public_key_pins.rb +5 -4
- data/lib/secure_headers/headers/referrer_policy.rb +37 -0
- 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 +16 -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 +15 -8
- data/lib/secure_headers.rb +139 -55
- data/lib/tasks/tasks.rake +7 -5
- 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 +89 -15
- 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 +80 -42
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +73 -0
- 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 +55 -17
- data/spec/lib/secure_headers/view_helpers_spec.rb +21 -8
- data/spec/lib/secure_headers_spec.rb +330 -58
- data/spec/spec_helper.rb +17 -25
- data/upgrading-to-3-0.md +13 -10
- data/upgrading-to-4-0.md +55 -0
- metadata +34 -7
|
@@ -1,33 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "policy_management"
|
|
3
|
+
require_relative "content_security_policy_config"
|
|
4
|
+
require "useragent"
|
|
2
5
|
|
|
3
6
|
module SecureHeaders
|
|
4
|
-
class ContentSecurityPolicyConfigError < StandardError; end
|
|
5
7
|
class ContentSecurityPolicy
|
|
6
8
|
include PolicyManagement
|
|
7
9
|
|
|
10
|
+
# constants to be used for version-specific UA sniffing
|
|
11
|
+
VERSION_46 = ::UserAgent::Version.new("46")
|
|
12
|
+
VERSION_10 = ::UserAgent::Version.new("10")
|
|
13
|
+
FALLBACK_VERSION = ::UserAgent::Version.new("0")
|
|
14
|
+
|
|
8
15
|
def initialize(config = nil, user_agent = OTHER)
|
|
9
|
-
config =
|
|
10
|
-
|
|
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
|
+
|
|
11
28
|
@parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
|
|
12
29
|
user_agent
|
|
13
30
|
else
|
|
14
31
|
UserAgent.parse(user_agent)
|
|
15
32
|
end
|
|
16
|
-
@
|
|
17
|
-
@preserve_schemes = @config
|
|
18
|
-
@script_nonce = @config
|
|
19
|
-
@style_nonce = @config
|
|
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
|
|
20
37
|
end
|
|
21
38
|
|
|
22
39
|
##
|
|
23
40
|
# Returns the name to use for the header. Either "Content-Security-Policy" or
|
|
24
41
|
# "Content-Security-Policy-Report-Only"
|
|
25
42
|
def name
|
|
26
|
-
|
|
27
|
-
REPORT_ONLY
|
|
28
|
-
else
|
|
29
|
-
HEADER_NAME
|
|
30
|
-
end
|
|
43
|
+
@config.class.const_get(:HEADER_NAME)
|
|
31
44
|
end
|
|
32
45
|
|
|
33
46
|
##
|
|
@@ -42,6 +55,14 @@ module SecureHeaders
|
|
|
42
55
|
|
|
43
56
|
private
|
|
44
57
|
|
|
58
|
+
def normalize_child_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."
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@config.frame_src || @config.child_src
|
|
64
|
+
end
|
|
65
|
+
|
|
45
66
|
# Private: converts the config object into a string representing a policy.
|
|
46
67
|
# Places default-src at the first directive and report-uri as the last. All
|
|
47
68
|
# others are presented in alphabetical order.
|
|
@@ -53,44 +74,90 @@ module SecureHeaders
|
|
|
53
74
|
directives.map do |directive_name|
|
|
54
75
|
case DIRECTIVE_VALUE_TYPES[directive_name]
|
|
55
76
|
when :boolean
|
|
56
|
-
symbol_to_hyphen_case(directive_name)
|
|
57
|
-
when :
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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)
|
|
61
84
|
end
|
|
62
85
|
end.compact.join("; ")
|
|
63
86
|
end
|
|
64
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
|
+
|
|
65
121
|
# Private: builds a string that represents one directive in a minified form.
|
|
66
122
|
#
|
|
67
123
|
# directive_name - a symbol representing the various ALL_DIRECTIVES
|
|
68
124
|
#
|
|
69
125
|
# Returns a string representing a directive.
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
75
139
|
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
78
144
|
end
|
|
79
145
|
|
|
80
146
|
# If a directive contains *, all other values are omitted.
|
|
81
147
|
# If a directive contains 'none' but has other values, 'none' is ommitted.
|
|
82
148
|
# Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
|
|
83
149
|
def minify_source_list(directive, source_list)
|
|
150
|
+
source_list = source_list.compact
|
|
84
151
|
if source_list.include?(STAR)
|
|
85
152
|
keep_wildcard_sources(source_list)
|
|
86
153
|
else
|
|
87
|
-
populate_nonces
|
|
88
|
-
reject_all_values_if_none
|
|
154
|
+
source_list = populate_nonces(directive, source_list)
|
|
155
|
+
source_list = reject_all_values_if_none(source_list)
|
|
89
156
|
|
|
90
157
|
unless directive == REPORT_URI || @preserve_schemes
|
|
91
|
-
strip_source_schemes
|
|
158
|
+
source_list = strip_source_schemes(source_list)
|
|
92
159
|
end
|
|
93
|
-
dedup_source_list(source_list)
|
|
160
|
+
dedup_source_list(source_list)
|
|
94
161
|
end
|
|
95
162
|
end
|
|
96
163
|
|
|
@@ -100,8 +167,12 @@ module SecureHeaders
|
|
|
100
167
|
end
|
|
101
168
|
|
|
102
169
|
# Discard any 'none' values if more directives are supplied since none may override values.
|
|
103
|
-
def reject_all_values_if_none
|
|
104
|
-
|
|
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
|
|
105
176
|
end
|
|
106
177
|
|
|
107
178
|
# Removes duplicates and sources that already match an existing wild card.
|
|
@@ -123,12 +194,14 @@ module SecureHeaders
|
|
|
123
194
|
|
|
124
195
|
# Private: append a nonce to the script/style directories if script_nonce
|
|
125
196
|
# or style_nonce are provided.
|
|
126
|
-
def populate_nonces
|
|
197
|
+
def populate_nonces(directive, source_list)
|
|
127
198
|
case directive
|
|
128
199
|
when SCRIPT_SRC
|
|
129
200
|
append_nonce(source_list, @script_nonce)
|
|
130
201
|
when STYLE_SRC
|
|
131
202
|
append_nonce(source_list, @style_nonce)
|
|
203
|
+
else
|
|
204
|
+
source_list
|
|
132
205
|
end
|
|
133
206
|
end
|
|
134
207
|
|
|
@@ -145,34 +218,50 @@ module SecureHeaders
|
|
|
145
218
|
source_list << UNSAFE_INLINE
|
|
146
219
|
end
|
|
147
220
|
end
|
|
221
|
+
|
|
222
|
+
source_list
|
|
148
223
|
end
|
|
149
224
|
|
|
150
225
|
# Private: return the list of directives that are supported by the user agent,
|
|
151
226
|
# starting with default-src and ending with report-uri.
|
|
152
227
|
def directives
|
|
153
|
-
[
|
|
228
|
+
[
|
|
229
|
+
DEFAULT_SRC,
|
|
154
230
|
BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
|
|
155
|
-
REPORT_URI
|
|
231
|
+
REPORT_URI
|
|
232
|
+
].flatten
|
|
156
233
|
end
|
|
157
234
|
|
|
158
235
|
# Private: Remove scheme from source expressions.
|
|
159
|
-
def strip_source_schemes
|
|
160
|
-
source_list.map
|
|
236
|
+
def strip_source_schemes(source_list)
|
|
237
|
+
source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
|
|
161
238
|
end
|
|
162
239
|
|
|
163
240
|
# Private: determine which directives are supported for the given user agent.
|
|
164
241
|
#
|
|
242
|
+
# Add UA-sniffing special casing here.
|
|
243
|
+
#
|
|
165
244
|
# Returns an array of symbols representing the directives.
|
|
166
245
|
def supported_directives
|
|
167
|
-
@supported_directives ||= VARIATIONS[@parsed_ua.browser]
|
|
246
|
+
@supported_directives ||= if VARIATIONS[@parsed_ua.browser]
|
|
247
|
+
if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
|
|
248
|
+
VARIATIONS["FirefoxTransitional"]
|
|
249
|
+
elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10)
|
|
250
|
+
VARIATIONS["SafariTransitional"]
|
|
251
|
+
else
|
|
252
|
+
VARIATIONS[@parsed_ua.browser]
|
|
253
|
+
end
|
|
254
|
+
else
|
|
255
|
+
VARIATIONS[OTHER]
|
|
256
|
+
end
|
|
168
257
|
end
|
|
169
258
|
|
|
170
259
|
def nonces_supported?
|
|
171
|
-
@nonces_supported ||=
|
|
260
|
+
@nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
|
|
172
261
|
end
|
|
173
262
|
|
|
174
263
|
def symbol_to_hyphen_case(sym)
|
|
175
|
-
sym.to_s.tr(
|
|
264
|
+
sym.to_s.tr("_", "-")
|
|
176
265
|
end
|
|
177
266
|
end
|
|
178
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
|