secure_headers 5.0.5 → 6.0.0.alpha02
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/.travis.yml +8 -4
- data/CHANGELOG.md +5 -1
- data/README.md +2 -2
- data/docs/upgrading-to-6-0.md +54 -0
- data/lib/secure_headers/configuration.rb +114 -164
- data/lib/secure_headers/headers/clear_site_data.rb +1 -3
- data/lib/secure_headers/headers/content_security_policy.rb +2 -9
- data/lib/secure_headers/headers/content_security_policy_config.rb +3 -13
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +2 -3
- data/lib/secure_headers/headers/policy_management.rb +12 -11
- data/lib/secure_headers/headers/public_key_pins.rb +2 -3
- data/lib/secure_headers/headers/referrer_policy.rb +2 -2
- data/lib/secure_headers/headers/strict_transport_security.rb +2 -2
- data/lib/secure_headers/headers/x_content_type_options.rb +2 -2
- data/lib/secure_headers/headers/x_download_options.rb +2 -2
- data/lib/secure_headers/headers/x_frame_options.rb +1 -2
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +2 -2
- data/lib/secure_headers/headers/x_xss_protection.rb +3 -3
- data/lib/secure_headers/view_helper.rb +9 -8
- data/lib/secure_headers.rb +14 -76
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/configuration_spec.rb +15 -70
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +12 -12
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +33 -7
- data/spec/lib/secure_headers/middleware_spec.rb +7 -1
- data/spec/lib/secure_headers/view_helpers_spec.rb +29 -0
- data/spec/lib/secure_headers_spec.rb +39 -40
- data/spec/spec_helper.rb +7 -3
- metadata +5 -4
|
@@ -5,7 +5,6 @@ module SecureHeaders
|
|
|
5
5
|
base.extend(ClassMethods)
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
MODERN_BROWSERS = %w(Chrome Opera Firefox)
|
|
9
8
|
DEFAULT_CONFIG = {
|
|
10
9
|
default_src: %w(https:),
|
|
11
10
|
img_src: %w(https: data: 'self'),
|
|
@@ -200,7 +199,8 @@ module SecureHeaders
|
|
|
200
199
|
#
|
|
201
200
|
# Returns a default policy if no configuration is provided, or a
|
|
202
201
|
# header name and value based on the config.
|
|
203
|
-
def make_header(config, user_agent)
|
|
202
|
+
def make_header(config, user_agent = nil)
|
|
203
|
+
return if config.nil? || config == OPT_OUT
|
|
204
204
|
header = new(config, user_agent)
|
|
205
205
|
[header.name, header.value]
|
|
206
206
|
end
|
|
@@ -215,27 +215,28 @@ module SecureHeaders
|
|
|
215
215
|
if config.directive_value(:script_src).nil?
|
|
216
216
|
raise ContentSecurityPolicyConfigError.new(":script_src is required, falling back to default-src is too dangerous. Use `script_src: OPT_OUT` to override")
|
|
217
217
|
end
|
|
218
|
+
if !config.report_only? && config.directive_value(:report_only)
|
|
219
|
+
raise ContentSecurityPolicyConfigError.new("Only the csp_report_only config should set :report_only to true")
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
if config.report_only? && config.directive_value(:report_only) == false
|
|
223
|
+
raise ContentSecurityPolicyConfigError.new("csp_report_only config must have :report_only set to true")
|
|
224
|
+
end
|
|
218
225
|
|
|
219
226
|
ContentSecurityPolicyConfig.attrs.each do |key|
|
|
220
227
|
value = config.directive_value(key)
|
|
221
228
|
next unless value
|
|
229
|
+
|
|
222
230
|
if META_CONFIGS.include?(key)
|
|
223
231
|
raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
|
|
232
|
+
elsif NONCES.include?(key)
|
|
233
|
+
raise ContentSecurityPolicyConfigError.new("#{key} must be a non-nil value") if value.nil?
|
|
224
234
|
else
|
|
225
235
|
validate_directive!(key, value)
|
|
226
236
|
end
|
|
227
237
|
end
|
|
228
238
|
end
|
|
229
239
|
|
|
230
|
-
# Public: check if a user agent supports CSP nonces
|
|
231
|
-
#
|
|
232
|
-
# user_agent - a String or a UserAgent object
|
|
233
|
-
def nonces_supported?(user_agent)
|
|
234
|
-
user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String)
|
|
235
|
-
MODERN_BROWSERS.include?(user_agent.browser) ||
|
|
236
|
-
user_agent.browser == "Safari" && (user_agent.version || CSP::FALLBACK_VERSION) >= CSP::VERSION_10
|
|
237
|
-
end
|
|
238
|
-
|
|
239
240
|
# Public: combine the values from two different configs.
|
|
240
241
|
#
|
|
241
242
|
# original - the main config
|
|
@@ -5,15 +5,14 @@ module SecureHeaders
|
|
|
5
5
|
HEADER_NAME = "Public-Key-Pins".freeze
|
|
6
6
|
REPORT_ONLY = "Public-Key-Pins-Report-Only".freeze
|
|
7
7
|
HASH_ALGORITHMS = [:sha256].freeze
|
|
8
|
-
CONFIG_KEY = :hpkp
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
class << self
|
|
12
11
|
# Public: make an hpkp header name, value pair
|
|
13
12
|
#
|
|
14
13
|
# Returns nil if not configured, returns header name and value if configured.
|
|
15
|
-
def make_header(config)
|
|
16
|
-
return if config.nil?
|
|
14
|
+
def make_header(config, user_agent = nil)
|
|
15
|
+
return if config.nil? || config == OPT_OUT
|
|
17
16
|
header = new(config)
|
|
18
17
|
[header.name, header.value]
|
|
19
18
|
end
|
|
@@ -14,14 +14,14 @@ module SecureHeaders
|
|
|
14
14
|
origin-when-cross-origin
|
|
15
15
|
unsafe-url
|
|
16
16
|
)
|
|
17
|
-
CONFIG_KEY = :referrer_policy
|
|
18
17
|
|
|
19
18
|
class << self
|
|
20
19
|
# Public: generate an Referrer Policy header.
|
|
21
20
|
#
|
|
22
21
|
# Returns a default header if no configuration is provided, or a
|
|
23
22
|
# header name and value based on the config.
|
|
24
|
-
def make_header(config = nil)
|
|
23
|
+
def make_header(config = nil, user_agent = nil)
|
|
24
|
+
return if config == OPT_OUT
|
|
25
25
|
config ||= DEFAULT_VALUE
|
|
26
26
|
[HEADER_NAME, Array(config).join(", ")]
|
|
27
27
|
end
|
|
@@ -8,14 +8,14 @@ module SecureHeaders
|
|
|
8
8
|
DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE
|
|
9
9
|
VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i
|
|
10
10
|
MESSAGE = "The config value supplied for the HSTS header was invalid. Must match #{VALID_STS_HEADER}"
|
|
11
|
-
CONFIG_KEY = :hsts
|
|
12
11
|
|
|
13
12
|
class << self
|
|
14
13
|
# Public: generate an hsts header name, value pair.
|
|
15
14
|
#
|
|
16
15
|
# Returns a default header if no configuration is provided, or a
|
|
17
16
|
# header name and value based on the config.
|
|
18
|
-
def make_header(config = nil)
|
|
17
|
+
def make_header(config = nil, user_agent = nil)
|
|
18
|
+
return if config == OPT_OUT
|
|
19
19
|
[HEADER_NAME, config || DEFAULT_VALUE]
|
|
20
20
|
end
|
|
21
21
|
|
|
@@ -5,14 +5,14 @@ module SecureHeaders
|
|
|
5
5
|
class XContentTypeOptions
|
|
6
6
|
HEADER_NAME = "X-Content-Type-Options".freeze
|
|
7
7
|
DEFAULT_VALUE = "nosniff"
|
|
8
|
-
CONFIG_KEY = :x_content_type_options
|
|
9
8
|
|
|
10
9
|
class << self
|
|
11
10
|
# Public: generate an X-Content-Type-Options header.
|
|
12
11
|
#
|
|
13
12
|
# Returns a default header if no configuration is provided, or a
|
|
14
13
|
# header name and value based on the config.
|
|
15
|
-
def make_header(config = nil)
|
|
14
|
+
def make_header(config = nil, user_agent = nil)
|
|
15
|
+
return if config == OPT_OUT
|
|
16
16
|
[HEADER_NAME, config || DEFAULT_VALUE]
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -4,14 +4,14 @@ module SecureHeaders
|
|
|
4
4
|
class XDownloadOptions
|
|
5
5
|
HEADER_NAME = "X-Download-Options".freeze
|
|
6
6
|
DEFAULT_VALUE = "noopen"
|
|
7
|
-
CONFIG_KEY = :x_download_options
|
|
8
7
|
|
|
9
8
|
class << self
|
|
10
9
|
# Public: generate an X-Download-Options header.
|
|
11
10
|
#
|
|
12
11
|
# Returns a default header if no configuration is provided, or a
|
|
13
12
|
# header name and value based on the config.
|
|
14
|
-
def make_header(config = nil)
|
|
13
|
+
def make_header(config = nil, user_agent = nil)
|
|
14
|
+
return if config == OPT_OUT
|
|
15
15
|
[HEADER_NAME, config || DEFAULT_VALUE]
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -3,7 +3,6 @@ module SecureHeaders
|
|
|
3
3
|
class XFOConfigError < StandardError; end
|
|
4
4
|
class XFrameOptions
|
|
5
5
|
HEADER_NAME = "X-Frame-Options".freeze
|
|
6
|
-
CONFIG_KEY = :x_frame_options
|
|
7
6
|
SAMEORIGIN = "sameorigin"
|
|
8
7
|
DENY = "deny"
|
|
9
8
|
ALLOW_FROM = "allow-from"
|
|
@@ -16,7 +15,7 @@ module SecureHeaders
|
|
|
16
15
|
#
|
|
17
16
|
# Returns a default header if no configuration is provided, or a
|
|
18
17
|
# header name and value based on the config.
|
|
19
|
-
def make_header(config = nil)
|
|
18
|
+
def make_header(config = nil, user_agent = nil)
|
|
20
19
|
return if config == OPT_OUT
|
|
21
20
|
[HEADER_NAME, config || DEFAULT_VALUE]
|
|
22
21
|
end
|
|
@@ -5,14 +5,14 @@ module SecureHeaders
|
|
|
5
5
|
HEADER_NAME = "X-Permitted-Cross-Domain-Policies".freeze
|
|
6
6
|
DEFAULT_VALUE = "none"
|
|
7
7
|
VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename)
|
|
8
|
-
CONFIG_KEY = :x_permitted_cross_domain_policies
|
|
9
8
|
|
|
10
9
|
class << self
|
|
11
10
|
# Public: generate an X-Permitted-Cross-Domain-Policies header.
|
|
12
11
|
#
|
|
13
12
|
# Returns a default header if no configuration is provided, or a
|
|
14
13
|
# header name and value based on the config.
|
|
15
|
-
def make_header(config = nil)
|
|
14
|
+
def make_header(config = nil, user_agent = nil)
|
|
15
|
+
return if config == OPT_OUT
|
|
16
16
|
[HEADER_NAME, config || DEFAULT_VALUE]
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -4,15 +4,15 @@ module SecureHeaders
|
|
|
4
4
|
class XXssProtection
|
|
5
5
|
HEADER_NAME = "X-XSS-Protection".freeze
|
|
6
6
|
DEFAULT_VALUE = "1; mode=block"
|
|
7
|
-
VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/
|
|
8
|
-
CONFIG_KEY = :x_xss_protection
|
|
7
|
+
VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/
|
|
9
8
|
|
|
10
9
|
class << self
|
|
11
10
|
# Public: generate an X-Xss-Protection header.
|
|
12
11
|
#
|
|
13
12
|
# Returns a default header if no configuration is provided, or a
|
|
14
13
|
# header name and value based on the config.
|
|
15
|
-
def make_header(config = nil)
|
|
14
|
+
def make_header(config = nil, user_agent = nil)
|
|
15
|
+
return if config == OPT_OUT
|
|
16
16
|
[HEADER_NAME, config || DEFAULT_VALUE]
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -19,7 +19,7 @@ module SecureHeaders
|
|
|
19
19
|
#
|
|
20
20
|
# Returns an html-safe link tag with the nonce attribute.
|
|
21
21
|
def nonced_stylesheet_link_tag(*args, &block)
|
|
22
|
-
opts = extract_options(args).merge(nonce:
|
|
22
|
+
opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style))
|
|
23
23
|
|
|
24
24
|
stylesheet_link_tag(*args, opts, &block)
|
|
25
25
|
end
|
|
@@ -37,7 +37,7 @@ module SecureHeaders
|
|
|
37
37
|
#
|
|
38
38
|
# Returns an html-safe script tag with the nonce attribute.
|
|
39
39
|
def nonced_javascript_include_tag(*args, &block)
|
|
40
|
-
opts = extract_options(args).merge(nonce:
|
|
40
|
+
opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script))
|
|
41
41
|
|
|
42
42
|
javascript_include_tag(*args, opts, &block)
|
|
43
43
|
end
|
|
@@ -47,7 +47,7 @@ module SecureHeaders
|
|
|
47
47
|
#
|
|
48
48
|
# Returns an html-safe script tag with the nonce attribute.
|
|
49
49
|
def nonced_javascript_pack_tag(*args, &block)
|
|
50
|
-
opts = extract_options(args).merge(nonce:
|
|
50
|
+
opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script))
|
|
51
51
|
|
|
52
52
|
javascript_pack_tag(*args, opts, &block)
|
|
53
53
|
end
|
|
@@ -57,7 +57,7 @@ module SecureHeaders
|
|
|
57
57
|
#
|
|
58
58
|
# Returns an html-safe link tag with the nonce attribute.
|
|
59
59
|
def nonced_stylesheet_pack_tag(*args, &block)
|
|
60
|
-
opts = extract_options(args).merge(nonce:
|
|
60
|
+
opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style))
|
|
61
61
|
|
|
62
62
|
stylesheet_pack_tag(*args, opts, &block)
|
|
63
63
|
end
|
|
@@ -66,7 +66,7 @@ module SecureHeaders
|
|
|
66
66
|
# Instructs secure_headers to append a nonce to style/script-src directives.
|
|
67
67
|
#
|
|
68
68
|
# Returns a non-html-safe nonce value.
|
|
69
|
-
def
|
|
69
|
+
def _content_security_policy_nonce(type)
|
|
70
70
|
case type
|
|
71
71
|
when :script
|
|
72
72
|
SecureHeaders.content_security_policy_script_nonce(@_request)
|
|
@@ -74,13 +74,14 @@ module SecureHeaders
|
|
|
74
74
|
SecureHeaders.content_security_policy_style_nonce(@_request)
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
|
+
alias_method :content_security_policy_nonce, :_content_security_policy_nonce
|
|
77
78
|
|
|
78
79
|
def content_security_policy_script_nonce
|
|
79
|
-
|
|
80
|
+
_content_security_policy_nonce(:script)
|
|
80
81
|
end
|
|
81
82
|
|
|
82
83
|
def content_security_policy_style_nonce
|
|
83
|
-
|
|
84
|
+
_content_security_policy_nonce(:style)
|
|
84
85
|
end
|
|
85
86
|
|
|
86
87
|
##
|
|
@@ -152,7 +153,7 @@ module SecureHeaders
|
|
|
152
153
|
else
|
|
153
154
|
content_or_options.html_safe # :'(
|
|
154
155
|
end
|
|
155
|
-
content_tag type, content, options.merge(nonce:
|
|
156
|
+
content_tag type, content, options.merge(nonce: _content_security_policy_nonce(type))
|
|
156
157
|
end
|
|
157
158
|
|
|
158
159
|
def extract_options(args)
|
data/lib/secure_headers.rb
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
require "secure_headers/configuration"
|
|
3
2
|
require "secure_headers/hash_helper"
|
|
4
3
|
require "secure_headers/headers/cookie"
|
|
5
4
|
require "secure_headers/headers/public_key_pins"
|
|
@@ -18,6 +17,7 @@ require "secure_headers/railtie"
|
|
|
18
17
|
require "secure_headers/view_helper"
|
|
19
18
|
require "useragent"
|
|
20
19
|
require "singleton"
|
|
20
|
+
require "secure_headers/configuration"
|
|
21
21
|
|
|
22
22
|
# All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
|
|
23
23
|
# or ":optout_of_protection" as a config value to disable a given header
|
|
@@ -52,29 +52,9 @@ module SecureHeaders
|
|
|
52
52
|
HTTPS = "https".freeze
|
|
53
53
|
CSP = ContentSecurityPolicy
|
|
54
54
|
|
|
55
|
-
ALL_HEADER_CLASSES = [
|
|
56
|
-
ExpectCertificateTransparency,
|
|
57
|
-
ClearSiteData,
|
|
58
|
-
ContentSecurityPolicyConfig,
|
|
59
|
-
ContentSecurityPolicyReportOnlyConfig,
|
|
60
|
-
StrictTransportSecurity,
|
|
61
|
-
PublicKeyPins,
|
|
62
|
-
ReferrerPolicy,
|
|
63
|
-
XContentTypeOptions,
|
|
64
|
-
XDownloadOptions,
|
|
65
|
-
XFrameOptions,
|
|
66
|
-
XPermittedCrossDomainPolicies,
|
|
67
|
-
XXssProtection
|
|
68
|
-
].freeze
|
|
69
|
-
|
|
70
|
-
ALL_HEADERS_BESIDES_CSP = (
|
|
71
|
-
ALL_HEADER_CLASSES -
|
|
72
|
-
[ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig]
|
|
73
|
-
).freeze
|
|
74
|
-
|
|
75
55
|
# Headers set on http requests (excludes STS and HPKP)
|
|
76
|
-
|
|
77
|
-
|
|
56
|
+
HTTPS_HEADER_CLASSES =
|
|
57
|
+
[StrictTransportSecurity, PublicKeyPins].freeze
|
|
78
58
|
|
|
79
59
|
class << self
|
|
80
60
|
# Public: override a given set of directives for the current request. If a
|
|
@@ -153,7 +133,7 @@ module SecureHeaders
|
|
|
153
133
|
# Public: opts out of setting all headers by telling secure_headers to use
|
|
154
134
|
# the NOOP configuration.
|
|
155
135
|
def opt_out_of_all_protection(request)
|
|
156
|
-
use_secure_headers_override(request, Configuration::
|
|
136
|
+
use_secure_headers_override(request, Configuration::NOOP_OVERRIDE)
|
|
157
137
|
end
|
|
158
138
|
|
|
159
139
|
# Public: Builds the hash of headers that should be applied base on the
|
|
@@ -168,27 +148,16 @@ module SecureHeaders
|
|
|
168
148
|
def header_hash_for(request)
|
|
169
149
|
prevent_dup = true
|
|
170
150
|
config = config_for(request, prevent_dup)
|
|
171
|
-
|
|
151
|
+
config.validate_config!
|
|
172
152
|
user_agent = UserAgent.parse(request.user_agent)
|
|
153
|
+
headers = config.generate_headers(user_agent)
|
|
173
154
|
|
|
174
|
-
if
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if !config.csp_report_only.opt_out? && config.csp_report_only.modified?
|
|
179
|
-
headers = update_cached_csp(config.csp_report_only, headers, user_agent)
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
header_classes_for(request).each_with_object({}) do |klass, hash|
|
|
183
|
-
if header = headers[klass::CONFIG_KEY]
|
|
184
|
-
header_name, value = if klass == ContentSecurityPolicyConfig || klass == ContentSecurityPolicyReportOnlyConfig
|
|
185
|
-
csp_header_for_ua(header, user_agent)
|
|
186
|
-
else
|
|
187
|
-
header
|
|
188
|
-
end
|
|
189
|
-
hash[header_name] = value
|
|
155
|
+
if request.scheme != HTTPS
|
|
156
|
+
HTTPS_HEADER_CLASSES.each do |klass|
|
|
157
|
+
headers.delete(klass::HEADER_NAME)
|
|
190
158
|
end
|
|
191
159
|
end
|
|
160
|
+
headers
|
|
192
161
|
end
|
|
193
162
|
|
|
194
163
|
# Public: specify which named override will be used for this request.
|
|
@@ -196,11 +165,9 @@ module SecureHeaders
|
|
|
196
165
|
#
|
|
197
166
|
# name - the name of the previously configured override.
|
|
198
167
|
def use_secure_headers_override(request, name)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
raise ArgumentError.new("no override by the name of #{name} has been configured")
|
|
203
|
-
end
|
|
168
|
+
config = config_for(request)
|
|
169
|
+
config.override(name)
|
|
170
|
+
override_secure_headers_request_config(request, config)
|
|
204
171
|
end
|
|
205
172
|
|
|
206
173
|
# Public: gets or creates a nonce for CSP.
|
|
@@ -228,7 +195,7 @@ module SecureHeaders
|
|
|
228
195
|
# Falls back to the global config
|
|
229
196
|
def config_for(request, prevent_dup = false)
|
|
230
197
|
config = request.env[SECURE_HEADERS_CONFIG] ||
|
|
231
|
-
Configuration.
|
|
198
|
+
Configuration.send(:default_config)
|
|
232
199
|
|
|
233
200
|
|
|
234
201
|
# Global configs are frozen, per-request configs are not. When we're not
|
|
@@ -285,35 +252,6 @@ module SecureHeaders
|
|
|
285
252
|
def override_secure_headers_request_config(request, config)
|
|
286
253
|
request.env[SECURE_HEADERS_CONFIG] = config
|
|
287
254
|
end
|
|
288
|
-
|
|
289
|
-
# Private: determines which headers are applicable to a given request.
|
|
290
|
-
#
|
|
291
|
-
# Returns a list of classes whose corresponding header values are valid for
|
|
292
|
-
# this request.
|
|
293
|
-
def header_classes_for(request)
|
|
294
|
-
if request.scheme == HTTPS
|
|
295
|
-
ALL_HEADER_CLASSES
|
|
296
|
-
else
|
|
297
|
-
HTTP_HEADER_CLASSES
|
|
298
|
-
end
|
|
299
|
-
end
|
|
300
|
-
|
|
301
|
-
def update_cached_csp(config, headers, user_agent)
|
|
302
|
-
headers = Configuration.send(:deep_copy, headers)
|
|
303
|
-
headers[config.class::CONFIG_KEY] = {}
|
|
304
|
-
variation = ContentSecurityPolicy.ua_to_variation(user_agent)
|
|
305
|
-
headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent)
|
|
306
|
-
headers
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
# Private: chooses the applicable CSP header for the provided user agent.
|
|
310
|
-
#
|
|
311
|
-
# headers - a hash of header_config_key => [header_name, header_value]
|
|
312
|
-
#
|
|
313
|
-
# Returns a CSP [header, value] array
|
|
314
|
-
def csp_header_for_ua(headers, user_agent)
|
|
315
|
-
headers[ContentSecurityPolicy.ua_to_variation(user_agent)]
|
|
316
|
-
end
|
|
317
255
|
end
|
|
318
256
|
|
|
319
257
|
# These methods are mixed into controllers and delegate to the class method
|
data/secure_headers.gemspec
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
Gem::Specification.new do |gem|
|
|
4
4
|
gem.name = "secure_headers"
|
|
5
|
-
gem.version = "
|
|
5
|
+
gem.version = "6.0.0.alpha02"
|
|
6
6
|
gem.authors = ["Neil Matatall"]
|
|
7
7
|
gem.email = ["neil.matatall@gmail.com"]
|
|
8
8
|
gem.description = "Manages application of security headers with many safe defaults."
|
|
@@ -5,88 +5,33 @@ module SecureHeaders
|
|
|
5
5
|
describe Configuration do
|
|
6
6
|
before(:each) do
|
|
7
7
|
reset_config
|
|
8
|
-
Configuration.default
|
|
9
8
|
end
|
|
10
9
|
|
|
11
10
|
it "has a default config" do
|
|
12
|
-
expect(Configuration.
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
it "warns when using deprecated internal-ish #get API" do
|
|
16
|
-
expect(Kernel).to receive(:warn).once.with(/`#get` is deprecated/)
|
|
17
|
-
Configuration.get(Configuration::DEFAULT_CONFIG)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
it "has an 'noop' config" do
|
|
21
|
-
expect(Configuration.get(Configuration::NOOP_CONFIGURATION, internal: true)).to_not be_nil
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
it "precomputes headers upon creation" do
|
|
25
|
-
default_config = Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)
|
|
26
|
-
header_hash = default_config.cached_headers.each_with_object({}) do |(key, value), hash|
|
|
27
|
-
header_name, header_value = if key == :csp
|
|
28
|
-
value["Chrome"]
|
|
29
|
-
else
|
|
30
|
-
value
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
hash[header_name] = header_value
|
|
34
|
-
end
|
|
35
|
-
expect_default_values(header_hash)
|
|
11
|
+
expect(Configuration.default).to_not be_nil
|
|
36
12
|
end
|
|
37
13
|
|
|
38
|
-
it "
|
|
39
|
-
Configuration.
|
|
40
|
-
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
config = Configuration.get(:test_override, internal: true)
|
|
44
|
-
noop = Configuration.get(Configuration::NOOP_CONFIGURATION, internal: true)
|
|
45
|
-
[:csp, :csp_report_only, :cookies].each do |key|
|
|
46
|
-
expect(config.send(key)).to eq(noop.send(key))
|
|
47
|
-
end
|
|
14
|
+
it "has an 'noop' override" do
|
|
15
|
+
Configuration.default
|
|
16
|
+
expect(Configuration.overrides(Configuration::NOOP_OVERRIDE)).to_not be_nil
|
|
48
17
|
end
|
|
49
18
|
|
|
50
|
-
it "
|
|
51
|
-
Configuration.
|
|
52
|
-
|
|
19
|
+
it "dup results in a copy of the default config" do
|
|
20
|
+
Configuration.default
|
|
21
|
+
original_configuration = Configuration.send(:default_config)
|
|
22
|
+
configuration = Configuration.dup
|
|
23
|
+
expect(original_configuration).not_to be(configuration)
|
|
24
|
+
Configuration::CONFIG_ATTRIBUTES.each do |attr|
|
|
25
|
+
expect(original_configuration.send(attr)).to eq(configuration.send(attr))
|
|
53
26
|
end
|
|
54
|
-
|
|
55
|
-
expect(Configuration.get(Configuration::DEFAULT_CONFIG, internal: true).cached_headers).to_not eq(Configuration.get(:test_override, internal: true).cached_headers)
|
|
56
27
|
end
|
|
57
28
|
|
|
58
|
-
it "stores an override
|
|
29
|
+
it "stores an override" do
|
|
59
30
|
Configuration.override(:test_override) do |config|
|
|
60
31
|
config.x_frame_options = "DENY"
|
|
61
32
|
end
|
|
62
33
|
|
|
63
|
-
expect(Configuration.
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
it "deep dup's config values when overriding so the original cannot be modified" do
|
|
67
|
-
Configuration.override(:override) do |config|
|
|
68
|
-
config.csp[:default_src] << "'self'"
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
default = Configuration.get(Configuration::DEFAULT_CONFIG, internal: true)
|
|
72
|
-
override = Configuration.get(:override, internal: true)
|
|
73
|
-
|
|
74
|
-
expect(override.csp.directive_value(:default_src)).not_to be(default.csp.directive_value(:default_src))
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
it "allows you to override an override" do
|
|
78
|
-
Configuration.override(:override) do |config|
|
|
79
|
-
config.csp = { default_src: %w('self'), script_src: %w('self')}
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
Configuration.override(:second_override, :override) do |config|
|
|
83
|
-
config.csp = config.csp.merge(script_src: %w(example.org))
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
original_override = Configuration.get(:override, internal: true)
|
|
87
|
-
expect(original_override.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self'))
|
|
88
|
-
override_config = Configuration.get(:second_override, internal: true)
|
|
89
|
-
expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org))
|
|
34
|
+
expect(Configuration.overrides(:test_override)).to_not be_nil
|
|
90
35
|
end
|
|
91
36
|
|
|
92
37
|
it "deprecates the secure_cookies configuration" do
|
|
@@ -106,7 +51,7 @@ module SecureHeaders
|
|
|
106
51
|
config.cookies = OPT_OUT
|
|
107
52
|
end
|
|
108
53
|
|
|
109
|
-
config = Configuration.
|
|
54
|
+
config = Configuration.dup
|
|
110
55
|
expect(config.cookies).to eq(OPT_OUT)
|
|
111
56
|
end
|
|
112
57
|
|
|
@@ -115,7 +60,7 @@ module SecureHeaders
|
|
|
115
60
|
config.cookies = {httponly: true, secure: true, samesite: {lax: false}}
|
|
116
61
|
end
|
|
117
62
|
|
|
118
|
-
config = Configuration.
|
|
63
|
+
config = Configuration.dup
|
|
119
64
|
expect(config.cookies).to eq({httponly: true, secure: true, samesite: {lax: false}})
|
|
120
65
|
end
|
|
121
66
|
end
|
|
@@ -124,7 +124,7 @@ module SecureHeaders
|
|
|
124
124
|
|
|
125
125
|
it "supports strict-dynamic" do
|
|
126
126
|
csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}, USER_AGENTS[:chrome])
|
|
127
|
-
expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456'")
|
|
127
|
+
expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'")
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
context "browser sniffing" do
|
|
@@ -143,44 +143,44 @@ module SecureHeaders
|
|
|
143
143
|
|
|
144
144
|
it "does not filter any directives for Chrome" do
|
|
145
145
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
|
|
146
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
|
|
146
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
|
|
147
147
|
end
|
|
148
148
|
|
|
149
149
|
it "does not filter any directives for Opera" do
|
|
150
150
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
|
|
151
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
|
|
151
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
|
|
155
155
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
|
|
156
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
156
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
|
|
160
160
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
|
|
161
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
161
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
-
it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors,
|
|
164
|
+
it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, hash sources, and plugin-types for Edge" do
|
|
165
165
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
|
|
166
|
-
expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
166
|
+
expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
-
it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors,
|
|
169
|
+
it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, hash sources, and plugin-types for safari" do
|
|
170
170
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
|
|
171
|
-
expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
171
|
+
expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
172
172
|
end
|
|
173
173
|
|
|
174
|
-
it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests,
|
|
174
|
+
it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, and hash sources for safari 10 and higher" do
|
|
175
175
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10])
|
|
176
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; report-uri report-uri.com")
|
|
176
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
177
177
|
end
|
|
178
178
|
|
|
179
179
|
it "falls back to standard Firefox defaults when the useragent version is not present" do
|
|
180
180
|
ua = USER_AGENTS[:firefox].dup
|
|
181
181
|
allow(ua).to receive(:version).and_return(nil)
|
|
182
182
|
policy = ContentSecurityPolicy.new(complex_opts, ua)
|
|
183
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
183
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
184
184
|
end
|
|
185
185
|
end
|
|
186
186
|
end
|