secure_headers 3.0.3 → 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/.gitignore +0 -9
- data/.rspec +2 -0
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -1
- data/.travis.yml +17 -5
- data/CHANGELOG.md +287 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +13 -5
- data/Guardfile +1 -0
- data/LICENSE +4 -199
- data/README.md +85 -265
- 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 +188 -59
- data/lib/secure_headers/hash_helper.rb +11 -0
- data/lib/secure_headers/headers/clear_site_data.rb +55 -0
- data/lib/secure_headers/headers/content_security_policy.rb +136 -325
- data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
- data/lib/secure_headers/headers/cookie.rb +144 -0
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
- data/lib/secure_headers/headers/policy_management.rb +410 -0
- 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 +3 -2
- 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 +44 -0
- data/lib/secure_headers/railtie.rb +11 -6
- data/lib/secure_headers/utils/cookies_config.rb +95 -0
- data/lib/secure_headers/view_helper.rb +76 -5
- data/lib/secure_headers.rb +159 -99
- data/lib/tasks/tasks.rake +83 -0
- data/secure_headers.gemspec +13 -3
- data/spec/lib/secure_headers/configuration_spec.rb +24 -9
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +88 -189
- data/spec/lib/secure_headers/headers/cookie_spec.rb +182 -0
- data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +228 -0
- 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 +6 -5
- 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 +96 -6
- data/spec/lib/secure_headers/view_helpers_spec.rb +138 -0
- data/spec/lib/secure_headers_spec.rb +407 -58
- data/spec/spec_helper.rb +19 -12
- data/upgrading-to-3-0.md +13 -9
- data/upgrading-to-4-0.md +55 -0
- metadata +45 -9
- data/lib/secure_headers/padrino.rb +0 -13
- data/travis.sh +0 -10
|
@@ -1,320 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
require
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "policy_management"
|
|
3
|
+
require_relative "content_security_policy_config"
|
|
4
|
+
require "useragent"
|
|
5
5
|
|
|
6
6
|
module SecureHeaders
|
|
7
|
-
class ContentSecurityPolicyConfigError < StandardError; end
|
|
8
7
|
class ContentSecurityPolicy
|
|
9
|
-
|
|
10
|
-
DEFAULT_VALUE = "default-src https:".freeze
|
|
11
|
-
DEFAULT_CONFIG = { default_src: %w(https:) }.freeze
|
|
12
|
-
HEADER_NAME = "Content-Security-Policy".freeze
|
|
13
|
-
REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
|
|
14
|
-
HEADER_NAMES = [HEADER_NAME, REPORT_ONLY]
|
|
15
|
-
DATA_PROTOCOL = "data:".freeze
|
|
16
|
-
BLOB_PROTOCOL = "blob:".freeze
|
|
17
|
-
SELF = "'self'".freeze
|
|
18
|
-
NONE = "'none'".freeze
|
|
19
|
-
STAR = "*".freeze
|
|
20
|
-
UNSAFE_INLINE = "'unsafe-inline'".freeze
|
|
21
|
-
UNSAFE_EVAL = "'unsafe-eval'".freeze
|
|
8
|
+
include PolicyManagement
|
|
22
9
|
|
|
23
|
-
#
|
|
24
|
-
|
|
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")
|
|
25
14
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
IMG_SRC = :img_src
|
|
31
|
-
MEDIA_SRC = :media_src
|
|
32
|
-
OBJECT_SRC = :object_src
|
|
33
|
-
SANDBOX = :sandbox
|
|
34
|
-
SCRIPT_SRC = :script_src
|
|
35
|
-
STYLE_SRC = :style_src
|
|
36
|
-
REPORT_URI = :report_uri
|
|
37
|
-
|
|
38
|
-
DIRECTIVES_1_0 = [
|
|
39
|
-
DEFAULT_SRC,
|
|
40
|
-
CONNECT_SRC,
|
|
41
|
-
FONT_SRC,
|
|
42
|
-
FRAME_SRC,
|
|
43
|
-
IMG_SRC,
|
|
44
|
-
MEDIA_SRC,
|
|
45
|
-
OBJECT_SRC,
|
|
46
|
-
SANDBOX,
|
|
47
|
-
SCRIPT_SRC,
|
|
48
|
-
STYLE_SRC,
|
|
49
|
-
REPORT_URI
|
|
50
|
-
].freeze
|
|
51
|
-
|
|
52
|
-
BASE_URI = :base_uri
|
|
53
|
-
CHILD_SRC = :child_src
|
|
54
|
-
FORM_ACTION = :form_action
|
|
55
|
-
FRAME_ANCESTORS = :frame_ancestors
|
|
56
|
-
PLUGIN_TYPES = :plugin_types
|
|
57
|
-
|
|
58
|
-
# These are directives that do not inherit the default-src value. This is
|
|
59
|
-
# useful when calling #combine_policies.
|
|
60
|
-
NON_DEFAULT_SOURCES = [
|
|
61
|
-
BASE_URI,
|
|
62
|
-
FORM_ACTION,
|
|
63
|
-
FRAME_ANCESTORS,
|
|
64
|
-
PLUGIN_TYPES,
|
|
65
|
-
REPORT_URI
|
|
66
|
-
]
|
|
67
|
-
|
|
68
|
-
DIRECTIVES_2_0 = [
|
|
69
|
-
DIRECTIVES_1_0,
|
|
70
|
-
BASE_URI,
|
|
71
|
-
CHILD_SRC,
|
|
72
|
-
FORM_ACTION,
|
|
73
|
-
FRAME_ANCESTORS,
|
|
74
|
-
PLUGIN_TYPES
|
|
75
|
-
].flatten.freeze
|
|
76
|
-
|
|
77
|
-
# All the directives currently under consideration for CSP level 3.
|
|
78
|
-
# https://w3c.github.io/webappsec/specs/CSP2/
|
|
79
|
-
MANIFEST_SRC = :manifest_src
|
|
80
|
-
REFLECTED_XSS = :reflected_xss
|
|
81
|
-
DIRECTIVES_3_0 = [
|
|
82
|
-
DIRECTIVES_2_0,
|
|
83
|
-
MANIFEST_SRC,
|
|
84
|
-
REFLECTED_XSS
|
|
85
|
-
].flatten.freeze
|
|
86
|
-
|
|
87
|
-
# All the directives that are not currently in a formal spec, but have
|
|
88
|
-
# been implemented somewhere.
|
|
89
|
-
BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
|
|
90
|
-
UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
|
|
91
|
-
DIRECTIVES_DRAFT = [
|
|
92
|
-
BLOCK_ALL_MIXED_CONTENT,
|
|
93
|
-
UPGRADE_INSECURE_REQUESTS
|
|
94
|
-
].freeze
|
|
95
|
-
|
|
96
|
-
SAFARI_DIRECTIVES = DIRECTIVES_1_0
|
|
97
|
-
|
|
98
|
-
FIREFOX_UNSUPPORTED_DIRECTIVES = [
|
|
99
|
-
BLOCK_ALL_MIXED_CONTENT,
|
|
100
|
-
CHILD_SRC,
|
|
101
|
-
PLUGIN_TYPES
|
|
102
|
-
].freeze
|
|
103
|
-
|
|
104
|
-
FIREFOX_DIRECTIVES = (
|
|
105
|
-
DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES
|
|
106
|
-
).freeze
|
|
107
|
-
|
|
108
|
-
CHROME_DIRECTIVES = (
|
|
109
|
-
DIRECTIVES_2_0 + DIRECTIVES_DRAFT
|
|
110
|
-
).freeze
|
|
111
|
-
|
|
112
|
-
ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort
|
|
113
|
-
|
|
114
|
-
# Think of default-src and report-uri as the beginning and end respectively,
|
|
115
|
-
# everything else is in between.
|
|
116
|
-
BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
|
|
117
|
-
|
|
118
|
-
VARIATIONS = {
|
|
119
|
-
"Chrome" => CHROME_DIRECTIVES,
|
|
120
|
-
"Opera" => CHROME_DIRECTIVES,
|
|
121
|
-
"Firefox" => FIREFOX_DIRECTIVES,
|
|
122
|
-
"Safari" => SAFARI_DIRECTIVES,
|
|
123
|
-
"Other" => CHROME_DIRECTIVES
|
|
124
|
-
}.freeze
|
|
125
|
-
|
|
126
|
-
OTHER = "Other".freeze
|
|
127
|
-
|
|
128
|
-
DIRECTIVE_VALUE_TYPES = {
|
|
129
|
-
BASE_URI => :source_list,
|
|
130
|
-
BLOCK_ALL_MIXED_CONTENT => :boolean,
|
|
131
|
-
CHILD_SRC => :source_list,
|
|
132
|
-
CONNECT_SRC => :source_list,
|
|
133
|
-
DEFAULT_SRC => :source_list,
|
|
134
|
-
FONT_SRC => :source_list,
|
|
135
|
-
FORM_ACTION => :source_list,
|
|
136
|
-
FRAME_ANCESTORS => :source_list,
|
|
137
|
-
FRAME_SRC => :source_list,
|
|
138
|
-
IMG_SRC => :source_list,
|
|
139
|
-
MANIFEST_SRC => :source_list,
|
|
140
|
-
MEDIA_SRC => :source_list,
|
|
141
|
-
OBJECT_SRC => :source_list,
|
|
142
|
-
PLUGIN_TYPES => :source_list,
|
|
143
|
-
REFLECTED_XSS => :string,
|
|
144
|
-
REPORT_URI => :source_list,
|
|
145
|
-
SANDBOX => :string,
|
|
146
|
-
SCRIPT_SRC => :source_list,
|
|
147
|
-
STYLE_SRC => :source_list,
|
|
148
|
-
UPGRADE_INSECURE_REQUESTS => :boolean
|
|
149
|
-
}.freeze
|
|
150
|
-
|
|
151
|
-
CONFIG_KEY = :csp
|
|
152
|
-
STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
|
|
153
|
-
HTTP_SCHEME_REGEX = %r{\Ahttps?://}
|
|
154
|
-
|
|
155
|
-
WILDCARD_SOURCES = [
|
|
156
|
-
UNSAFE_EVAL,
|
|
157
|
-
UNSAFE_INLINE,
|
|
158
|
-
STAR,
|
|
159
|
-
DATA_PROTOCOL,
|
|
160
|
-
BLOB_PROTOCOL
|
|
161
|
-
].freeze
|
|
162
|
-
|
|
163
|
-
META_CONFIGS = [
|
|
164
|
-
:report_only,
|
|
165
|
-
:preserve_schemes
|
|
166
|
-
].freeze
|
|
167
|
-
|
|
168
|
-
class << self
|
|
169
|
-
# Public: generate a header name, value array that is user-agent-aware.
|
|
170
|
-
#
|
|
171
|
-
# Returns a default policy if no configuration is provided, or a
|
|
172
|
-
# header name and value based on the config.
|
|
173
|
-
def make_header(config, user_agent)
|
|
174
|
-
header = new(config, user_agent)
|
|
175
|
-
[header.name, header.value]
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
# Public: Validates each source expression.
|
|
179
|
-
#
|
|
180
|
-
# Does not validate the invididual values of the source expression (e.g.
|
|
181
|
-
# script_src => h*t*t*p: will not raise an exception)
|
|
182
|
-
def validate_config!(config)
|
|
183
|
-
return if config.nil? || config == OPT_OUT
|
|
184
|
-
raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src]
|
|
185
|
-
config.each do |key, value|
|
|
186
|
-
if META_CONFIGS.include?(key)
|
|
187
|
-
raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
|
|
188
|
-
else
|
|
189
|
-
validate_directive!(key, value)
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
# Public: determine if merging +additions+ will cause a change to the
|
|
195
|
-
# actual value of the config.
|
|
196
|
-
#
|
|
197
|
-
# e.g. config = { script_src: %w(example.org google.com)} and
|
|
198
|
-
# additions = { script_src: %w(google.com)} then idempotent_additions? would return
|
|
199
|
-
# because google.com is already in the config.
|
|
200
|
-
def idempotent_additions?(config, additions)
|
|
201
|
-
return false if config == OPT_OUT
|
|
202
|
-
config.to_s == combine_policies(config, additions).to_s
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
# Public: combine the values from two different configs.
|
|
206
|
-
#
|
|
207
|
-
# original - the main config
|
|
208
|
-
# additions - values to be merged in
|
|
209
|
-
#
|
|
210
|
-
# raises an error if the original config is OPT_OUT
|
|
211
|
-
#
|
|
212
|
-
# 1. for non-source-list values (report_only, block_all_mixed_content, upgrade_insecure_requests),
|
|
213
|
-
# additions will overwrite the original value.
|
|
214
|
-
# 2. if a value in additions does not exist in the original config, the
|
|
215
|
-
# default-src value is included to match original behavior.
|
|
216
|
-
# 3. if a value in additions does exist in the original config, the two
|
|
217
|
-
# values are joined.
|
|
218
|
-
def combine_policies(original, additions)
|
|
219
|
-
if original == OPT_OUT
|
|
220
|
-
raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
original = original.dup if original.frozen?
|
|
224
|
-
|
|
225
|
-
# in case we would be appending to an empty directive, fill it with the default-src value
|
|
226
|
-
additions.keys.each do |directive|
|
|
227
|
-
unless original[directive] || !source_list?(directive) || NON_DEFAULT_SOURCES.include?(directive)
|
|
228
|
-
original[directive] = original[:default_src]
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
# merge the two hashes. combine (instead of overwrite) the array values
|
|
233
|
-
# when each hash contains a value for a given key.
|
|
234
|
-
original.merge(additions) do |directive, lhs, rhs|
|
|
235
|
-
if source_list?(directive)
|
|
236
|
-
(lhs.to_a + rhs.to_a).compact.uniq
|
|
237
|
-
else
|
|
238
|
-
rhs
|
|
239
|
-
end
|
|
240
|
-
end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
private
|
|
244
|
-
|
|
245
|
-
def source_list?(directive)
|
|
246
|
-
DIRECTIVE_VALUE_TYPES[directive] == :source_list
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
# Private: Validates that the configuration has a valid type, or that it is a valid
|
|
250
|
-
# source expression.
|
|
251
|
-
def validate_directive!(key, value)
|
|
252
|
-
case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[key]
|
|
253
|
-
when :boolean
|
|
254
|
-
unless boolean?(value)
|
|
255
|
-
raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value")
|
|
256
|
-
end
|
|
257
|
-
when :string
|
|
258
|
-
unless value.is_a?(String)
|
|
259
|
-
raise ContentSecurityPolicyConfigError.new("#{key} Must be a string. Found #{config.class}: #{config} value")
|
|
260
|
-
end
|
|
15
|
+
def initialize(config = nil, user_agent = OTHER)
|
|
16
|
+
@config = if config.is_a?(Hash)
|
|
17
|
+
if config[:report_only]
|
|
18
|
+
ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
|
|
261
19
|
else
|
|
262
|
-
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
# Private: validates that a source expression:
|
|
267
|
-
# 1. has a valid name
|
|
268
|
-
# 2. is an array of strings
|
|
269
|
-
# 3. does not contain any depreated, now invalid values (inline, eval, self, none)
|
|
270
|
-
#
|
|
271
|
-
# Does not validate the invididual values of the source expression (e.g.
|
|
272
|
-
# script_src => h*t*t*p: will not raise an exception)
|
|
273
|
-
def validate_source_expression!(key, value)
|
|
274
|
-
unless ContentSecurityPolicy::ALL_DIRECTIVES.include?(key)
|
|
275
|
-
raise ContentSecurityPolicyConfigError.new("Unknown directive #{key}")
|
|
20
|
+
ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG)
|
|
276
21
|
end
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
value.each do |source_expression|
|
|
283
|
-
if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression)
|
|
284
|
-
raise ContentSecurityPolicyConfigError.new("#{key} contains an invalid keyword source (#{source_expression}). This value must be single quoted.")
|
|
285
|
-
end
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def boolean?(value)
|
|
290
|
-
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
22
|
+
elsif config.nil?
|
|
23
|
+
ContentSecurityPolicyConfig.new(DEFAULT_CONFIG)
|
|
24
|
+
else
|
|
25
|
+
config
|
|
291
26
|
end
|
|
292
|
-
end
|
|
293
27
|
|
|
294
|
-
|
|
295
|
-
def initialize(config = nil, user_agent = OTHER)
|
|
296
|
-
config = Configuration.deep_copy(DEFAULT_CONFIG) unless config
|
|
297
|
-
@config = config
|
|
298
28
|
@parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
|
|
299
29
|
user_agent
|
|
300
30
|
else
|
|
301
31
|
UserAgent.parse(user_agent)
|
|
302
32
|
end
|
|
303
|
-
@
|
|
304
|
-
@preserve_schemes = @config
|
|
305
|
-
@script_nonce = @config
|
|
306
|
-
@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
|
|
307
37
|
end
|
|
308
38
|
|
|
309
39
|
##
|
|
310
40
|
# Returns the name to use for the header. Either "Content-Security-Policy" or
|
|
311
41
|
# "Content-Security-Policy-Report-Only"
|
|
312
42
|
def name
|
|
313
|
-
|
|
314
|
-
REPORT_ONLY
|
|
315
|
-
else
|
|
316
|
-
HEADER_NAME
|
|
317
|
-
end
|
|
43
|
+
@config.class.const_get(:HEADER_NAME)
|
|
318
44
|
end
|
|
319
45
|
|
|
320
46
|
##
|
|
@@ -329,6 +55,14 @@ module SecureHeaders
|
|
|
329
55
|
|
|
330
56
|
private
|
|
331
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
|
+
|
|
332
66
|
# Private: converts the config object into a string representing a policy.
|
|
333
67
|
# Places default-src at the first directive and report-uri as the last. All
|
|
334
68
|
# others are presented in alphabetical order.
|
|
@@ -340,46 +74,105 @@ module SecureHeaders
|
|
|
340
74
|
directives.map do |directive_name|
|
|
341
75
|
case DIRECTIVE_VALUE_TYPES[directive_name]
|
|
342
76
|
when :boolean
|
|
343
|
-
symbol_to_hyphen_case(directive_name)
|
|
344
|
-
when :
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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)
|
|
348
84
|
end
|
|
349
85
|
end.compact.join("; ")
|
|
350
86
|
end
|
|
351
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
|
+
|
|
352
121
|
# Private: builds a string that represents one directive in a minified form.
|
|
353
|
-
# If a directive contains *, all other values are omitted.
|
|
354
|
-
# If a directive contains 'none' but has other values, 'none' is ommitted.
|
|
355
|
-
# Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
|
|
356
122
|
#
|
|
357
123
|
# directive_name - a symbol representing the various ALL_DIRECTIVES
|
|
358
124
|
#
|
|
359
125
|
# Returns a string representing a directive.
|
|
360
|
-
def
|
|
361
|
-
|
|
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
|
|
362
139
|
|
|
363
|
-
source_list
|
|
364
|
-
|
|
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
|
|
144
|
+
end
|
|
365
145
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
146
|
+
# If a directive contains *, all other values are omitted.
|
|
147
|
+
# If a directive contains 'none' but has other values, 'none' is ommitted.
|
|
148
|
+
# Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
|
|
149
|
+
def minify_source_list(directive, source_list)
|
|
150
|
+
source_list = source_list.compact
|
|
151
|
+
if source_list.include?(STAR)
|
|
152
|
+
keep_wildcard_sources(source_list)
|
|
369
153
|
else
|
|
370
|
-
populate_nonces(
|
|
371
|
-
|
|
372
|
-
# Discard any 'none' values if more directives are supplied since none may override values.
|
|
373
|
-
source_list.reject! { |value| value == NONE } if source_list.length > 1
|
|
154
|
+
source_list = populate_nonces(directive, source_list)
|
|
155
|
+
source_list = reject_all_values_if_none(source_list)
|
|
374
156
|
|
|
375
|
-
|
|
376
|
-
unless directive_name == REPORT_URI || @preserve_schemes
|
|
157
|
+
unless directive == REPORT_URI || @preserve_schemes
|
|
377
158
|
source_list = strip_source_schemes(source_list)
|
|
378
159
|
end
|
|
379
|
-
dedup_source_list(source_list)
|
|
160
|
+
dedup_source_list(source_list)
|
|
380
161
|
end
|
|
162
|
+
end
|
|
381
163
|
|
|
382
|
-
|
|
164
|
+
# Discard trailing entries (excluding unsafe-*) since * accomplishes the same.
|
|
165
|
+
def keep_wildcard_sources(source_list)
|
|
166
|
+
source_list.select { |value| WILDCARD_SOURCES.include?(value) }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Discard any 'none' values if more directives are supplied since none may override values.
|
|
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
|
|
383
176
|
end
|
|
384
177
|
|
|
385
178
|
# Removes duplicates and sources that already match an existing wild card.
|
|
@@ -407,6 +200,8 @@ module SecureHeaders
|
|
|
407
200
|
append_nonce(source_list, @script_nonce)
|
|
408
201
|
when STYLE_SRC
|
|
409
202
|
append_nonce(source_list, @style_nonce)
|
|
203
|
+
else
|
|
204
|
+
source_list
|
|
410
205
|
end
|
|
411
206
|
end
|
|
412
207
|
|
|
@@ -423,14 +218,18 @@ module SecureHeaders
|
|
|
423
218
|
source_list << UNSAFE_INLINE
|
|
424
219
|
end
|
|
425
220
|
end
|
|
221
|
+
|
|
222
|
+
source_list
|
|
426
223
|
end
|
|
427
224
|
|
|
428
225
|
# Private: return the list of directives that are supported by the user agent,
|
|
429
226
|
# starting with default-src and ending with report-uri.
|
|
430
227
|
def directives
|
|
431
|
-
[
|
|
228
|
+
[
|
|
229
|
+
DEFAULT_SRC,
|
|
432
230
|
BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
|
|
433
|
-
REPORT_URI
|
|
231
|
+
REPORT_URI
|
|
232
|
+
].flatten
|
|
434
233
|
end
|
|
435
234
|
|
|
436
235
|
# Private: Remove scheme from source expressions.
|
|
@@ -440,17 +239,29 @@ module SecureHeaders
|
|
|
440
239
|
|
|
441
240
|
# Private: determine which directives are supported for the given user agent.
|
|
442
241
|
#
|
|
242
|
+
# Add UA-sniffing special casing here.
|
|
243
|
+
#
|
|
443
244
|
# Returns an array of symbols representing the directives.
|
|
444
245
|
def supported_directives
|
|
445
|
-
@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
|
|
446
257
|
end
|
|
447
258
|
|
|
448
259
|
def nonces_supported?
|
|
449
|
-
@nonces_supported ||=
|
|
260
|
+
@nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
|
|
450
261
|
end
|
|
451
262
|
|
|
452
263
|
def symbol_to_hyphen_case(sym)
|
|
453
|
-
sym.to_s.tr(
|
|
264
|
+
sym.to_s.tr("_", "-")
|
|
454
265
|
end
|
|
455
266
|
end
|
|
456
267
|
end
|