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
|
@@ -7,9 +7,6 @@ module SecureHeaders
|
|
|
7
7
|
MODERN_BROWSERS = %w(Chrome Opera Firefox)
|
|
8
8
|
DEFAULT_VALUE = "default-src https:".freeze
|
|
9
9
|
DEFAULT_CONFIG = { default_src: %w(https:) }.freeze
|
|
10
|
-
HEADER_NAME = "Content-Security-Policy".freeze
|
|
11
|
-
REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
|
|
12
|
-
HEADER_NAMES = [HEADER_NAME, REPORT_ONLY]
|
|
13
10
|
DATA_PROTOCOL = "data:".freeze
|
|
14
11
|
BLOB_PROTOCOL = "blob:".freeze
|
|
15
12
|
SELF = "'self'".freeze
|
|
@@ -17,6 +14,7 @@ module SecureHeaders
|
|
|
17
14
|
STAR = "*".freeze
|
|
18
15
|
UNSAFE_INLINE = "'unsafe-inline'".freeze
|
|
19
16
|
UNSAFE_EVAL = "'unsafe-eval'".freeze
|
|
17
|
+
STRICT_DYNAMIC = "'strict-dynamic'".freeze
|
|
20
18
|
|
|
21
19
|
# leftover deprecated values that will be in common use upon upgrading.
|
|
22
20
|
DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze
|
|
@@ -53,16 +51,6 @@ module SecureHeaders
|
|
|
53
51
|
FRAME_ANCESTORS = :frame_ancestors
|
|
54
52
|
PLUGIN_TYPES = :plugin_types
|
|
55
53
|
|
|
56
|
-
# These are directives that do not inherit the default-src value. This is
|
|
57
|
-
# useful when calling #combine_policies.
|
|
58
|
-
NON_FETCH_SOURCES = [
|
|
59
|
-
BASE_URI,
|
|
60
|
-
FORM_ACTION,
|
|
61
|
-
FRAME_ANCESTORS,
|
|
62
|
-
PLUGIN_TYPES,
|
|
63
|
-
REPORT_URI
|
|
64
|
-
]
|
|
65
|
-
|
|
66
54
|
DIRECTIVES_2_0 = [
|
|
67
55
|
DIRECTIVES_1_0,
|
|
68
56
|
BASE_URI,
|
|
@@ -74,40 +62,53 @@ module SecureHeaders
|
|
|
74
62
|
|
|
75
63
|
# All the directives currently under consideration for CSP level 3.
|
|
76
64
|
# https://w3c.github.io/webappsec/specs/CSP2/
|
|
65
|
+
BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
|
|
77
66
|
MANIFEST_SRC = :manifest_src
|
|
78
|
-
|
|
67
|
+
UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
|
|
68
|
+
WORKER_SRC = :worker_src
|
|
69
|
+
|
|
79
70
|
DIRECTIVES_3_0 = [
|
|
80
71
|
DIRECTIVES_2_0,
|
|
81
|
-
MANIFEST_SRC,
|
|
82
|
-
REFLECTED_XSS
|
|
83
|
-
].flatten.freeze
|
|
84
|
-
|
|
85
|
-
# All the directives that are not currently in a formal spec, but have
|
|
86
|
-
# been implemented somewhere.
|
|
87
|
-
BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
|
|
88
|
-
UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
|
|
89
|
-
DIRECTIVES_DRAFT = [
|
|
90
72
|
BLOCK_ALL_MIXED_CONTENT,
|
|
73
|
+
MANIFEST_SRC,
|
|
74
|
+
WORKER_SRC,
|
|
91
75
|
UPGRADE_INSECURE_REQUESTS
|
|
92
|
-
].freeze
|
|
76
|
+
].flatten.freeze
|
|
93
77
|
|
|
78
|
+
EDGE_DIRECTIVES = DIRECTIVES_1_0
|
|
94
79
|
SAFARI_DIRECTIVES = DIRECTIVES_1_0
|
|
80
|
+
SAFARI_10_DIRECTIVES = DIRECTIVES_2_0
|
|
95
81
|
|
|
96
82
|
FIREFOX_UNSUPPORTED_DIRECTIVES = [
|
|
97
83
|
BLOCK_ALL_MIXED_CONTENT,
|
|
98
84
|
CHILD_SRC,
|
|
85
|
+
WORKER_SRC,
|
|
86
|
+
PLUGIN_TYPES
|
|
87
|
+
].freeze
|
|
88
|
+
|
|
89
|
+
FIREFOX_46_DEPRECATED_DIRECTIVES = [
|
|
90
|
+
FRAME_SRC
|
|
91
|
+
].freeze
|
|
92
|
+
|
|
93
|
+
FIREFOX_46_UNSUPPORTED_DIRECTIVES = [
|
|
94
|
+
BLOCK_ALL_MIXED_CONTENT,
|
|
95
|
+
WORKER_SRC,
|
|
99
96
|
PLUGIN_TYPES
|
|
100
97
|
].freeze
|
|
101
98
|
|
|
102
99
|
FIREFOX_DIRECTIVES = (
|
|
103
|
-
|
|
100
|
+
DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
|
|
101
|
+
).freeze
|
|
102
|
+
|
|
103
|
+
FIREFOX_46_DIRECTIVES = (
|
|
104
|
+
DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
|
|
104
105
|
).freeze
|
|
105
106
|
|
|
106
107
|
CHROME_DIRECTIVES = (
|
|
107
|
-
|
|
108
|
+
DIRECTIVES_3_0
|
|
108
109
|
).freeze
|
|
109
110
|
|
|
110
|
-
ALL_DIRECTIVES =
|
|
111
|
+
ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort
|
|
111
112
|
|
|
112
113
|
# Think of default-src and report-uri as the beginning and end respectively,
|
|
113
114
|
# everything else is in between.
|
|
@@ -117,7 +118,10 @@ module SecureHeaders
|
|
|
117
118
|
"Chrome" => CHROME_DIRECTIVES,
|
|
118
119
|
"Opera" => CHROME_DIRECTIVES,
|
|
119
120
|
"Firefox" => FIREFOX_DIRECTIVES,
|
|
121
|
+
"FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
|
|
120
122
|
"Safari" => SAFARI_DIRECTIVES,
|
|
123
|
+
"SafariTransitional" => SAFARI_10_DIRECTIVES,
|
|
124
|
+
"Edge" => EDGE_DIRECTIVES,
|
|
121
125
|
"Other" => CHROME_DIRECTIVES
|
|
122
126
|
}.freeze
|
|
123
127
|
|
|
@@ -137,16 +141,32 @@ module SecureHeaders
|
|
|
137
141
|
MANIFEST_SRC => :source_list,
|
|
138
142
|
MEDIA_SRC => :source_list,
|
|
139
143
|
OBJECT_SRC => :source_list,
|
|
140
|
-
PLUGIN_TYPES => :
|
|
141
|
-
REFLECTED_XSS => :string,
|
|
144
|
+
PLUGIN_TYPES => :media_type_list,
|
|
142
145
|
REPORT_URI => :source_list,
|
|
143
|
-
SANDBOX => :
|
|
146
|
+
SANDBOX => :sandbox_list,
|
|
144
147
|
SCRIPT_SRC => :source_list,
|
|
145
148
|
STYLE_SRC => :source_list,
|
|
149
|
+
WORKER_SRC => :source_list,
|
|
146
150
|
UPGRADE_INSECURE_REQUESTS => :boolean
|
|
147
151
|
}.freeze
|
|
148
152
|
|
|
149
|
-
|
|
153
|
+
# These are directives that don't have use a source list, and hence do not
|
|
154
|
+
# inherit the default-src value.
|
|
155
|
+
NON_SOURCE_LIST_SOURCES = DIRECTIVE_VALUE_TYPES.select do |_, type|
|
|
156
|
+
type != :source_list
|
|
157
|
+
end.keys.freeze
|
|
158
|
+
|
|
159
|
+
# These are directives that take a source list, but that do not inherit
|
|
160
|
+
# the default-src value.
|
|
161
|
+
NON_FETCH_SOURCES = [
|
|
162
|
+
BASE_URI,
|
|
163
|
+
FORM_ACTION,
|
|
164
|
+
FRAME_ANCESTORS,
|
|
165
|
+
REPORT_URI
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES - NON_SOURCE_LIST_SOURCES
|
|
169
|
+
|
|
150
170
|
STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
|
|
151
171
|
HTTP_SCHEME_REGEX = %r{\Ahttps?://}
|
|
152
172
|
|
|
@@ -163,6 +183,11 @@ module SecureHeaders
|
|
|
163
183
|
:preserve_schemes
|
|
164
184
|
].freeze
|
|
165
185
|
|
|
186
|
+
NONCES = [
|
|
187
|
+
:script_nonce,
|
|
188
|
+
:style_nonce
|
|
189
|
+
].freeze
|
|
190
|
+
|
|
166
191
|
module ClassMethods
|
|
167
192
|
# Public: generate a header name, value array that is user-agent-aware.
|
|
168
193
|
#
|
|
@@ -178,9 +203,11 @@ module SecureHeaders
|
|
|
178
203
|
# Does not validate the invididual values of the source expression (e.g.
|
|
179
204
|
# script_src => h*t*t*p: will not raise an exception)
|
|
180
205
|
def validate_config!(config)
|
|
181
|
-
return if config.nil? || config
|
|
182
|
-
raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config
|
|
183
|
-
|
|
206
|
+
return if config.nil? || config.opt_out?
|
|
207
|
+
raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src)
|
|
208
|
+
ContentSecurityPolicyConfig.attrs.each do |key|
|
|
209
|
+
value = config.directive_value(key)
|
|
210
|
+
next unless value
|
|
184
211
|
if META_CONFIGS.include?(key)
|
|
185
212
|
raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
|
|
186
213
|
else
|
|
@@ -189,16 +216,13 @@ module SecureHeaders
|
|
|
189
216
|
end
|
|
190
217
|
end
|
|
191
218
|
|
|
192
|
-
# Public:
|
|
193
|
-
# actual value of the config.
|
|
219
|
+
# Public: check if a user agent supports CSP nonces
|
|
194
220
|
#
|
|
195
|
-
#
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return false if config == OPT_OUT
|
|
201
|
-
config == combine_policies(config, additions)
|
|
221
|
+
# user_agent - a String or a UserAgent object
|
|
222
|
+
def nonces_supported?(user_agent)
|
|
223
|
+
user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String)
|
|
224
|
+
MODERN_BROWSERS.include?(user_agent.browser) ||
|
|
225
|
+
user_agent.browser == "Safari" && (user_agent.version || CSP::FALLBACK_VERSION) >= CSP::VERSION_10
|
|
202
226
|
end
|
|
203
227
|
|
|
204
228
|
# Public: combine the values from two different configs.
|
|
@@ -215,7 +239,7 @@ module SecureHeaders
|
|
|
215
239
|
# 3. if a value in additions does exist in the original config, the two
|
|
216
240
|
# values are joined.
|
|
217
241
|
def combine_policies(original, additions)
|
|
218
|
-
if original ==
|
|
242
|
+
if original == {}
|
|
219
243
|
raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
|
|
220
244
|
end
|
|
221
245
|
|
|
@@ -239,7 +263,7 @@ module SecureHeaders
|
|
|
239
263
|
# when each hash contains a value for a given key.
|
|
240
264
|
def merge_policy_additions(original, additions)
|
|
241
265
|
original.merge(additions) do |directive, lhs, rhs|
|
|
242
|
-
if
|
|
266
|
+
if list_directive?(directive)
|
|
243
267
|
(lhs.to_a + rhs.to_a).compact.uniq
|
|
244
268
|
else
|
|
245
269
|
rhs
|
|
@@ -247,48 +271,108 @@ module SecureHeaders
|
|
|
247
271
|
end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
|
|
248
272
|
end
|
|
249
273
|
|
|
274
|
+
# Returns True if a directive expects a list of values and False otherwise.
|
|
275
|
+
def list_directive?(directive)
|
|
276
|
+
source_list?(directive) ||
|
|
277
|
+
sandbox_list?(directive) ||
|
|
278
|
+
media_type_list?(directive)
|
|
279
|
+
end
|
|
280
|
+
|
|
250
281
|
# For each directive in additions that does not exist in the original config,
|
|
251
282
|
# copy the default-src value to the original config. This modifies the original hash.
|
|
252
283
|
def populate_fetch_source_with_default!(original, additions)
|
|
253
284
|
# in case we would be appending to an empty directive, fill it with the default-src value
|
|
254
|
-
additions.
|
|
255
|
-
|
|
256
|
-
|
|
285
|
+
additions.each_key do |directive|
|
|
286
|
+
directive = if directive.to_s.end_with?("_nonce")
|
|
287
|
+
directive.to_s.gsub(/_nonce/, "_src").to_sym
|
|
288
|
+
else
|
|
289
|
+
directive
|
|
290
|
+
end
|
|
291
|
+
# Don't set a default if directive has an existing value
|
|
292
|
+
next if original[directive]
|
|
293
|
+
if FETCH_SOURCES.include?(directive)
|
|
294
|
+
original[directive] = default_for(directive, original)
|
|
257
295
|
end
|
|
258
296
|
end
|
|
259
297
|
end
|
|
260
298
|
|
|
299
|
+
def default_for(directive, original)
|
|
300
|
+
return original[FRAME_SRC] if directive == CHILD_SRC && original[FRAME_SRC]
|
|
301
|
+
return original[CHILD_SRC] if directive == FRAME_SRC && original[CHILD_SRC]
|
|
302
|
+
original[DEFAULT_SRC]
|
|
303
|
+
end
|
|
304
|
+
|
|
261
305
|
def source_list?(directive)
|
|
262
306
|
DIRECTIVE_VALUE_TYPES[directive] == :source_list
|
|
263
307
|
end
|
|
264
308
|
|
|
309
|
+
def sandbox_list?(directive)
|
|
310
|
+
DIRECTIVE_VALUE_TYPES[directive] == :sandbox_list
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def media_type_list?(directive)
|
|
314
|
+
DIRECTIVE_VALUE_TYPES[directive] == :media_type_list
|
|
315
|
+
end
|
|
316
|
+
|
|
265
317
|
# Private: Validates that the configuration has a valid type, or that it is a valid
|
|
266
318
|
# source expression.
|
|
267
|
-
def validate_directive!(directive,
|
|
319
|
+
def validate_directive!(directive, value)
|
|
320
|
+
ensure_valid_directive!(directive)
|
|
268
321
|
case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive]
|
|
269
322
|
when :boolean
|
|
270
|
-
unless boolean?(
|
|
271
|
-
raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean value")
|
|
272
|
-
end
|
|
273
|
-
when :string
|
|
274
|
-
unless source_expression.is_a?(String)
|
|
275
|
-
raise ContentSecurityPolicyConfigError.new("#{directive} Must be a string. Found #{config.class}: #{config} value")
|
|
323
|
+
unless boolean?(value)
|
|
324
|
+
raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean. Found #{value.class} value")
|
|
276
325
|
end
|
|
326
|
+
when :sandbox_list
|
|
327
|
+
validate_sandbox_expression!(directive, value)
|
|
328
|
+
when :media_type_list
|
|
329
|
+
validate_media_type_expression!(directive, value)
|
|
330
|
+
when :source_list
|
|
331
|
+
validate_source_expression!(directive, value)
|
|
277
332
|
else
|
|
278
|
-
|
|
333
|
+
raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Private: validates that a sandbox token expression:
|
|
338
|
+
# 1. is an array of strings or optionally `true` (to enable maximal sandboxing)
|
|
339
|
+
# 2. For arrays, each element is of the form allow-*
|
|
340
|
+
def validate_sandbox_expression!(directive, sandbox_token_expression)
|
|
341
|
+
# We support sandbox: true to indicate a maximally secure sandbox.
|
|
342
|
+
return if boolean?(sandbox_token_expression) && sandbox_token_expression == true
|
|
343
|
+
ensure_array_of_strings!(directive, sandbox_token_expression)
|
|
344
|
+
valid = sandbox_token_expression.compact.all? do |v|
|
|
345
|
+
v.is_a?(String) && v.start_with?("allow-")
|
|
346
|
+
end
|
|
347
|
+
if !valid
|
|
348
|
+
raise ContentSecurityPolicyConfigError.new("#{directive} must be True or an array of zero or more sandbox token strings (ex. allow-forms)")
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Private: validates that a media type expression:
|
|
353
|
+
# 1. is an array of strings
|
|
354
|
+
# 2. each element is of the form type/subtype
|
|
355
|
+
def validate_media_type_expression!(directive, media_type_expression)
|
|
356
|
+
ensure_array_of_strings!(directive, media_type_expression)
|
|
357
|
+
valid = media_type_expression.compact.all? do |v|
|
|
358
|
+
# All media types are of the form: <type from RFC 2045> "/" <subtype from RFC 2045>.
|
|
359
|
+
v =~ /\A.+\/.+\z/
|
|
360
|
+
end
|
|
361
|
+
if !valid
|
|
362
|
+
raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of valid media types (ex. application/pdf)")
|
|
279
363
|
end
|
|
280
364
|
end
|
|
281
365
|
|
|
282
366
|
# Private: validates that a source expression:
|
|
283
|
-
# 1.
|
|
284
|
-
# 2.
|
|
285
|
-
# 3. does not contain any depreated, now invalid values (inline, eval, self, none)
|
|
367
|
+
# 1. is an array of strings
|
|
368
|
+
# 2. does not contain any deprecated, now invalid values (inline, eval, self, none)
|
|
286
369
|
#
|
|
287
370
|
# Does not validate the invididual values of the source expression (e.g.
|
|
288
371
|
# script_src => h*t*t*p: will not raise an exception)
|
|
289
372
|
def validate_source_expression!(directive, source_expression)
|
|
290
|
-
|
|
291
|
-
|
|
373
|
+
if source_expression != OPT_OUT
|
|
374
|
+
ensure_array_of_strings!(directive, source_expression)
|
|
375
|
+
end
|
|
292
376
|
ensure_valid_sources!(directive, source_expression)
|
|
293
377
|
end
|
|
294
378
|
|
|
@@ -298,16 +382,16 @@ module SecureHeaders
|
|
|
298
382
|
end
|
|
299
383
|
end
|
|
300
384
|
|
|
301
|
-
def ensure_array_of_strings!(directive,
|
|
302
|
-
|
|
385
|
+
def ensure_array_of_strings!(directive, value)
|
|
386
|
+
if (!value.is_a?(Array) || !value.compact.all? { |v| v.is_a?(String) })
|
|
303
387
|
raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of strings")
|
|
304
388
|
end
|
|
305
389
|
end
|
|
306
390
|
|
|
307
391
|
def ensure_valid_sources!(directive, source_expression)
|
|
308
|
-
source_expression.each do |
|
|
309
|
-
if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(
|
|
310
|
-
raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{
|
|
392
|
+
source_expression.each do |expression|
|
|
393
|
+
if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(expression)
|
|
394
|
+
raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{expression}). This value must be single quoted.")
|
|
311
395
|
end
|
|
312
396
|
end
|
|
313
397
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
class ReferrerPolicyConfigError < StandardError; end
|
|
3
|
+
class ReferrerPolicy
|
|
4
|
+
HEADER_NAME = "Referrer-Policy".freeze
|
|
5
|
+
DEFAULT_VALUE = "origin-when-cross-origin"
|
|
6
|
+
VALID_POLICIES = %w(
|
|
7
|
+
no-referrer
|
|
8
|
+
no-referrer-when-downgrade
|
|
9
|
+
same-origin
|
|
10
|
+
strict-origin
|
|
11
|
+
strict-origin-when-cross-origin
|
|
12
|
+
origin
|
|
13
|
+
origin-when-cross-origin
|
|
14
|
+
unsafe-url
|
|
15
|
+
)
|
|
16
|
+
CONFIG_KEY = :referrer_policy
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Public: generate an Referrer Policy header.
|
|
20
|
+
#
|
|
21
|
+
# Returns a default header if no configuration is provided, or a
|
|
22
|
+
# header name and value based on the config.
|
|
23
|
+
def make_header(config = nil)
|
|
24
|
+
[HEADER_NAME, config || DEFAULT_VALUE]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate_config!(config)
|
|
28
|
+
return if config.nil? || config == OPT_OUT
|
|
29
|
+
raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String)
|
|
30
|
+
unless VALID_POLICIES.include?(config.downcase)
|
|
31
|
+
raise ReferrerPolicyConfigError.new("Value can only be one of #{VALID_POLICIES.join(', ')}")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -2,7 +2,7 @@ module SecureHeaders
|
|
|
2
2
|
class STSConfigError < StandardError; end
|
|
3
3
|
|
|
4
4
|
class StrictTransportSecurity
|
|
5
|
-
HEADER_NAME = 'Strict-Transport-Security'
|
|
5
|
+
HEADER_NAME = 'Strict-Transport-Security'.freeze
|
|
6
6
|
HSTS_MAX_AGE = "631138519"
|
|
7
7
|
DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE
|
|
8
8
|
VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i
|
|
@@ -2,7 +2,7 @@ module SecureHeaders
|
|
|
2
2
|
class XContentTypeOptionsConfigError < StandardError; end
|
|
3
3
|
|
|
4
4
|
class XContentTypeOptions
|
|
5
|
-
HEADER_NAME = "X-Content-Type-Options"
|
|
5
|
+
HEADER_NAME = "X-Content-Type-Options".freeze
|
|
6
6
|
DEFAULT_VALUE = "nosniff"
|
|
7
7
|
CONFIG_KEY = :x_content_type_options
|
|
8
8
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module SecureHeaders
|
|
2
2
|
class XPCDPConfigError < StandardError; end
|
|
3
3
|
class XPermittedCrossDomainPolicies
|
|
4
|
-
HEADER_NAME = "X-Permitted-Cross-Domain-Policies"
|
|
4
|
+
HEADER_NAME = "X-Permitted-Cross-Domain-Policies".freeze
|
|
5
5
|
DEFAULT_VALUE = 'none'
|
|
6
6
|
VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename)
|
|
7
7
|
CONFIG_KEY = :x_permitted_cross_domain_policies
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module SecureHeaders
|
|
2
2
|
class XXssProtectionConfigError < StandardError; end
|
|
3
3
|
class XXssProtection
|
|
4
|
-
HEADER_NAME = 'X-XSS-Protection'
|
|
4
|
+
HEADER_NAME = 'X-XSS-Protection'.freeze
|
|
5
5
|
DEFAULT_VALUE = "1; mode=block"
|
|
6
6
|
VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i
|
|
7
7
|
CONFIG_KEY = :x_xss_protection
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module SecureHeaders
|
|
2
2
|
class Middleware
|
|
3
|
+
HPKP_SAME_HOST_WARNING = "[WARNING] HPKP report host should not be the same as the request host. See https://github.com/twitter/secureheaders/issues/166"
|
|
4
|
+
|
|
3
5
|
def initialize(app)
|
|
4
6
|
@app = app
|
|
5
7
|
end
|
|
@@ -10,6 +12,10 @@ module SecureHeaders
|
|
|
10
12
|
status, headers, response = @app.call(env)
|
|
11
13
|
|
|
12
14
|
config = SecureHeaders.config_for(req)
|
|
15
|
+
if config.hpkp_report_host == req.host
|
|
16
|
+
Kernel.warn(HPKP_SAME_HOST_WARNING)
|
|
17
|
+
end
|
|
18
|
+
|
|
13
19
|
flag_cookies!(headers, override_secure(env, config.cookies)) if config.cookies
|
|
14
20
|
headers.merge!(SecureHeaders.header_hash_for(req))
|
|
15
21
|
[status, headers, response]
|
|
@@ -7,7 +7,7 @@ if defined?(Rails::Railtie)
|
|
|
7
7
|
'X-Permitted-Cross-Domain-Policies', 'X-Download-Options',
|
|
8
8
|
'X-Content-Type-Options', 'Strict-Transport-Security',
|
|
9
9
|
'Content-Security-Policy', 'Content-Security-Policy-Report-Only',
|
|
10
|
-
'Public-Key-Pins', 'Public-Key-Pins-Report-Only']
|
|
10
|
+
'Public-Key-Pins', 'Public-Key-Pins-Report-Only', 'Referrer-Policy']
|
|
11
11
|
|
|
12
12
|
initializer "secure_headers.middleware" do
|
|
13
13
|
Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware
|
|
@@ -41,10 +41,12 @@ module SecureHeaders
|
|
|
41
41
|
|
|
42
42
|
# when configuring with booleans, only one enforcement is permitted
|
|
43
43
|
def validate_samesite_boolean_config!
|
|
44
|
-
if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict)
|
|
45
|
-
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax
|
|
46
|
-
elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax)
|
|
47
|
-
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax
|
|
44
|
+
if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && (config[:samesite].key?(:strict) || config[:samesite].key?(:none))
|
|
45
|
+
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax with strict or no enforcement is not permitted.")
|
|
46
|
+
elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && (config[:samesite].key?(:lax) || config[:samesite].key?(:none))
|
|
47
|
+
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure strict with lax or no enforcement is not permitted.")
|
|
48
|
+
elsif config[:samesite].key?(:none) && config[:samesite][:none].is_a?(TrueClass) && (config[:samesite].key?(:lax) || config[:samesite].key?(:strict))
|
|
49
|
+
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure no enforcement with lax or strict is not permitted.")
|
|
48
50
|
end
|
|
49
51
|
end
|
|
50
52
|
|
|
@@ -34,6 +34,14 @@ module SecureHeaders
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def content_security_policy_script_nonce
|
|
38
|
+
content_security_policy_nonce(:script)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def content_security_policy_style_nonce
|
|
42
|
+
content_security_policy_nonce(:style)
|
|
43
|
+
end
|
|
44
|
+
|
|
37
45
|
##
|
|
38
46
|
# Checks to see if the hashed code is expected and adds the hash source
|
|
39
47
|
# value to the current CSP.
|
|
@@ -87,9 +95,9 @@ module SecureHeaders
|
|
|
87
95
|
<<-EOF
|
|
88
96
|
\n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
|
|
89
97
|
#{content}
|
|
90
|
-
*** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/
|
|
98
|
+
*** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/secure_headers_generated_hashes.yml:***
|
|
91
99
|
#{file_path}:
|
|
92
|
-
- #{hash_value}\n\n
|
|
100
|
+
- \"#{hash_value}\"\n\n
|
|
93
101
|
NOTE: dynamic javascript is not supported using script hash integration
|
|
94
102
|
on purpose. It defeats the point of using it in the first place.
|
|
95
103
|
EOF
|
|
@@ -108,8 +116,6 @@ module SecureHeaders
|
|
|
108
116
|
end
|
|
109
117
|
end
|
|
110
118
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
end
|
|
115
|
-
end
|
|
119
|
+
ActiveSupport.on_load :action_view do
|
|
120
|
+
include SecureHeaders::ViewHelpers
|
|
121
|
+
end if defined?(ActiveSupport)
|