secure_headers 5.0.5 → 6.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/.travis.yml +8 -4
- data/CHANGELOG.md +5 -1
- data/README.md +2 -2
- data/docs/upgrading-to-6-0.md +50 -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 +6 -66
- 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 +14 -65
- 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 -78
- data/secure_headers.gemspec +1 -2
- data/spec/lib/secure_headers/configuration_spec.rb +15 -70
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +2 -65
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +35 -9
- 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 +38 -76
- data/spec/spec_helper.rb +7 -3
- metadata +3 -16
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
module SecureHeaders
|
|
3
3
|
module DynamicConfig
|
|
4
4
|
def self.included(base)
|
|
5
|
-
base.send(:attr_writer, :modified)
|
|
6
5
|
base.send(:attr_reader, *base.attrs)
|
|
7
6
|
base.attrs.each do |attr|
|
|
8
7
|
base.send(:define_method, "#{attr}=") do |value|
|
|
@@ -42,7 +41,6 @@ module SecureHeaders
|
|
|
42
41
|
@upgrade_insecure_requests = nil
|
|
43
42
|
|
|
44
43
|
from_hash(hash)
|
|
45
|
-
@modified = false
|
|
46
44
|
end
|
|
47
45
|
|
|
48
46
|
def update_directive(directive, value)
|
|
@@ -55,12 +53,10 @@ module SecureHeaders
|
|
|
55
53
|
end
|
|
56
54
|
end
|
|
57
55
|
|
|
58
|
-
def modified?
|
|
59
|
-
@modified
|
|
60
|
-
end
|
|
61
|
-
|
|
62
56
|
def merge(new_hash)
|
|
63
|
-
|
|
57
|
+
new_config = self.dup
|
|
58
|
+
new_config.send(:from_hash, new_hash)
|
|
59
|
+
new_config
|
|
64
60
|
end
|
|
65
61
|
|
|
66
62
|
def merge!(new_hash)
|
|
@@ -109,17 +105,12 @@ module SecureHeaders
|
|
|
109
105
|
def write_attribute(attr, value)
|
|
110
106
|
value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
|
|
111
107
|
attr_variable = "@#{attr}"
|
|
112
|
-
prev_value = self.instance_variable_get(attr_variable)
|
|
113
108
|
self.instance_variable_set(attr_variable, value)
|
|
114
|
-
if prev_value != value
|
|
115
|
-
@modified = true
|
|
116
|
-
end
|
|
117
109
|
end
|
|
118
110
|
end
|
|
119
111
|
|
|
120
112
|
class ContentSecurityPolicyConfigError < StandardError; end
|
|
121
113
|
class ContentSecurityPolicyConfig
|
|
122
|
-
CONFIG_KEY = :csp
|
|
123
114
|
HEADER_NAME = "Content-Security-Policy".freeze
|
|
124
115
|
|
|
125
116
|
ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
|
|
@@ -149,7 +140,6 @@ module SecureHeaders
|
|
|
149
140
|
end
|
|
150
141
|
|
|
151
142
|
class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig
|
|
152
|
-
CONFIG_KEY = :csp_report_only
|
|
153
143
|
HEADER_NAME = "Content-Security-Policy-Report-Only".freeze
|
|
154
144
|
|
|
155
145
|
def report_only?
|
|
@@ -4,7 +4,6 @@ module SecureHeaders
|
|
|
4
4
|
|
|
5
5
|
class ExpectCertificateTransparency
|
|
6
6
|
HEADER_NAME = "Expect-CT".freeze
|
|
7
|
-
CONFIG_KEY = :expect_certificate_transparency
|
|
8
7
|
INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze
|
|
9
8
|
INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze
|
|
10
9
|
REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze
|
|
@@ -15,8 +14,8 @@ module SecureHeaders
|
|
|
15
14
|
#
|
|
16
15
|
# Returns nil if not configured, returns header name and value if
|
|
17
16
|
# configured.
|
|
18
|
-
def make_header(config)
|
|
19
|
-
return if config.nil?
|
|
17
|
+
def make_header(config, use_agent = nil)
|
|
18
|
+
return if config.nil? || config == OPT_OUT
|
|
20
19
|
|
|
21
20
|
header = new(config)
|
|
22
21
|
[HEADER_NAME, header.value]
|
|
@@ -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'),
|
|
@@ -82,58 +81,12 @@ module SecureHeaders
|
|
|
82
81
|
UPGRADE_INSECURE_REQUESTS
|
|
83
82
|
].flatten.freeze
|
|
84
83
|
|
|
85
|
-
EDGE_DIRECTIVES = DIRECTIVES_1_0
|
|
86
|
-
SAFARI_DIRECTIVES = DIRECTIVES_1_0
|
|
87
|
-
SAFARI_10_DIRECTIVES = DIRECTIVES_2_0
|
|
88
|
-
|
|
89
|
-
FIREFOX_UNSUPPORTED_DIRECTIVES = [
|
|
90
|
-
BLOCK_ALL_MIXED_CONTENT,
|
|
91
|
-
CHILD_SRC,
|
|
92
|
-
WORKER_SRC,
|
|
93
|
-
PLUGIN_TYPES
|
|
94
|
-
].freeze
|
|
95
|
-
|
|
96
|
-
FIREFOX_46_DEPRECATED_DIRECTIVES = [
|
|
97
|
-
FRAME_SRC
|
|
98
|
-
].freeze
|
|
99
|
-
|
|
100
|
-
FIREFOX_46_UNSUPPORTED_DIRECTIVES = [
|
|
101
|
-
BLOCK_ALL_MIXED_CONTENT,
|
|
102
|
-
WORKER_SRC,
|
|
103
|
-
PLUGIN_TYPES
|
|
104
|
-
].freeze
|
|
105
|
-
|
|
106
|
-
FIREFOX_DIRECTIVES = (
|
|
107
|
-
DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
|
|
108
|
-
).freeze
|
|
109
|
-
|
|
110
|
-
FIREFOX_46_DIRECTIVES = (
|
|
111
|
-
DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
|
|
112
|
-
).freeze
|
|
113
|
-
|
|
114
|
-
CHROME_DIRECTIVES = (
|
|
115
|
-
DIRECTIVES_3_0
|
|
116
|
-
).freeze
|
|
117
|
-
|
|
118
84
|
ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort
|
|
119
85
|
|
|
120
86
|
# Think of default-src and report-uri as the beginning and end respectively,
|
|
121
87
|
# everything else is in between.
|
|
122
88
|
BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
|
|
123
89
|
|
|
124
|
-
VARIATIONS = {
|
|
125
|
-
"Chrome" => CHROME_DIRECTIVES,
|
|
126
|
-
"Opera" => CHROME_DIRECTIVES,
|
|
127
|
-
"Firefox" => FIREFOX_DIRECTIVES,
|
|
128
|
-
"FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
|
|
129
|
-
"Safari" => SAFARI_DIRECTIVES,
|
|
130
|
-
"SafariTransitional" => SAFARI_10_DIRECTIVES,
|
|
131
|
-
"Edge" => EDGE_DIRECTIVES,
|
|
132
|
-
"Other" => CHROME_DIRECTIVES
|
|
133
|
-
}.freeze
|
|
134
|
-
|
|
135
|
-
OTHER = "Other".freeze
|
|
136
|
-
|
|
137
90
|
DIRECTIVE_VALUE_TYPES = {
|
|
138
91
|
BASE_URI => :source_list,
|
|
139
92
|
BLOCK_ALL_MIXED_CONTENT => :boolean,
|
|
@@ -200,8 +153,9 @@ module SecureHeaders
|
|
|
200
153
|
#
|
|
201
154
|
# Returns a default policy if no configuration is provided, or a
|
|
202
155
|
# header name and value based on the config.
|
|
203
|
-
def make_header(config
|
|
204
|
-
|
|
156
|
+
def make_header(config)
|
|
157
|
+
return if config.nil? || config == OPT_OUT
|
|
158
|
+
header = new(config)
|
|
205
159
|
[header.name, header.value]
|
|
206
160
|
end
|
|
207
161
|
|
|
@@ -215,27 +169,28 @@ module SecureHeaders
|
|
|
215
169
|
if config.directive_value(:script_src).nil?
|
|
216
170
|
raise ContentSecurityPolicyConfigError.new(":script_src is required, falling back to default-src is too dangerous. Use `script_src: OPT_OUT` to override")
|
|
217
171
|
end
|
|
172
|
+
if !config.report_only? && config.directive_value(:report_only)
|
|
173
|
+
raise ContentSecurityPolicyConfigError.new("Only the csp_report_only config should set :report_only to true")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if config.report_only? && config.directive_value(:report_only) == false
|
|
177
|
+
raise ContentSecurityPolicyConfigError.new("csp_report_only config must have :report_only set to true")
|
|
178
|
+
end
|
|
218
179
|
|
|
219
180
|
ContentSecurityPolicyConfig.attrs.each do |key|
|
|
220
181
|
value = config.directive_value(key)
|
|
221
182
|
next unless value
|
|
183
|
+
|
|
222
184
|
if META_CONFIGS.include?(key)
|
|
223
185
|
raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
|
|
186
|
+
elsif NONCES.include?(key)
|
|
187
|
+
raise ContentSecurityPolicyConfigError.new("#{key} must be a non-nil value") if value.nil?
|
|
224
188
|
else
|
|
225
189
|
validate_directive!(key, value)
|
|
226
190
|
end
|
|
227
191
|
end
|
|
228
192
|
end
|
|
229
193
|
|
|
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
194
|
# Public: combine the values from two different configs.
|
|
240
195
|
#
|
|
241
196
|
# original - the main config
|
|
@@ -302,17 +257,11 @@ module SecureHeaders
|
|
|
302
257
|
# Don't set a default if directive has an existing value
|
|
303
258
|
next if original[directive]
|
|
304
259
|
if FETCH_SOURCES.include?(directive)
|
|
305
|
-
original[directive] =
|
|
260
|
+
original[directive] = original[DEFAULT_SRC]
|
|
306
261
|
end
|
|
307
262
|
end
|
|
308
263
|
end
|
|
309
264
|
|
|
310
|
-
def default_for(directive, original)
|
|
311
|
-
return original[FRAME_SRC] if directive == CHILD_SRC && original[FRAME_SRC]
|
|
312
|
-
return original[CHILD_SRC] if directive == FRAME_SRC && original[CHILD_SRC]
|
|
313
|
-
original[DEFAULT_SRC]
|
|
314
|
-
end
|
|
315
|
-
|
|
316
265
|
def source_list?(directive)
|
|
317
266
|
DIRECTIVE_VALUE_TYPES[directive] == :source_list
|
|
318
267
|
end
|
|
@@ -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"
|
|
@@ -16,8 +15,8 @@ require "secure_headers/headers/expect_certificate_transparency"
|
|
|
16
15
|
require "secure_headers/middleware"
|
|
17
16
|
require "secure_headers/railtie"
|
|
18
17
|
require "secure_headers/view_helper"
|
|
19
|
-
require "useragent"
|
|
20
18
|
require "singleton"
|
|
19
|
+
require "secure_headers/configuration"
|
|
21
20
|
|
|
22
21
|
# All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
|
|
23
22
|
# or ":optout_of_protection" as a config value to disable a given header
|
|
@@ -52,29 +51,9 @@ module SecureHeaders
|
|
|
52
51
|
HTTPS = "https".freeze
|
|
53
52
|
CSP = ContentSecurityPolicy
|
|
54
53
|
|
|
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
54
|
# Headers set on http requests (excludes STS and HPKP)
|
|
76
|
-
|
|
77
|
-
|
|
55
|
+
HTTPS_HEADER_CLASSES =
|
|
56
|
+
[StrictTransportSecurity, PublicKeyPins].freeze
|
|
78
57
|
|
|
79
58
|
class << self
|
|
80
59
|
# Public: override a given set of directives for the current request. If a
|
|
@@ -153,7 +132,7 @@ module SecureHeaders
|
|
|
153
132
|
# Public: opts out of setting all headers by telling secure_headers to use
|
|
154
133
|
# the NOOP configuration.
|
|
155
134
|
def opt_out_of_all_protection(request)
|
|
156
|
-
use_secure_headers_override(request, Configuration::
|
|
135
|
+
use_secure_headers_override(request, Configuration::NOOP_OVERRIDE)
|
|
157
136
|
end
|
|
158
137
|
|
|
159
138
|
# Public: Builds the hash of headers that should be applied base on the
|
|
@@ -168,27 +147,15 @@ module SecureHeaders
|
|
|
168
147
|
def header_hash_for(request)
|
|
169
148
|
prevent_dup = true
|
|
170
149
|
config = config_for(request, prevent_dup)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if !config.csp.opt_out? && config.csp.modified?
|
|
175
|
-
headers = update_cached_csp(config.csp, headers, user_agent)
|
|
176
|
-
end
|
|
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
|
|
150
|
+
config.validate_config!
|
|
151
|
+
headers = config.generate_headers
|
|
181
152
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
csp_header_for_ua(header, user_agent)
|
|
186
|
-
else
|
|
187
|
-
header
|
|
188
|
-
end
|
|
189
|
-
hash[header_name] = value
|
|
153
|
+
if request.scheme != HTTPS
|
|
154
|
+
HTTPS_HEADER_CLASSES.each do |klass|
|
|
155
|
+
headers.delete(klass::HEADER_NAME)
|
|
190
156
|
end
|
|
191
157
|
end
|
|
158
|
+
headers
|
|
192
159
|
end
|
|
193
160
|
|
|
194
161
|
# Public: specify which named override will be used for this request.
|
|
@@ -196,11 +163,9 @@ module SecureHeaders
|
|
|
196
163
|
#
|
|
197
164
|
# name - the name of the previously configured override.
|
|
198
165
|
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
|
|
166
|
+
config = config_for(request)
|
|
167
|
+
config.override(name)
|
|
168
|
+
override_secure_headers_request_config(request, config)
|
|
204
169
|
end
|
|
205
170
|
|
|
206
171
|
# Public: gets or creates a nonce for CSP.
|
|
@@ -228,7 +193,7 @@ module SecureHeaders
|
|
|
228
193
|
# Falls back to the global config
|
|
229
194
|
def config_for(request, prevent_dup = false)
|
|
230
195
|
config = request.env[SECURE_HEADERS_CONFIG] ||
|
|
231
|
-
Configuration.
|
|
196
|
+
Configuration.send(:default_config)
|
|
232
197
|
|
|
233
198
|
|
|
234
199
|
# Global configs are frozen, per-request configs are not. When we're not
|
|
@@ -285,35 +250,6 @@ module SecureHeaders
|
|
|
285
250
|
def override_secure_headers_request_config(request, config)
|
|
286
251
|
request.env[SECURE_HEADERS_CONFIG] = config
|
|
287
252
|
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
253
|
end
|
|
318
254
|
|
|
319
255
|
# 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"
|
|
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."
|
|
@@ -16,5 +16,4 @@ Gem::Specification.new do |gem|
|
|
|
16
16
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
17
17
|
gem.require_paths = ["lib"]
|
|
18
18
|
gem.add_development_dependency "rake"
|
|
19
|
-
gem.add_dependency "useragent", ">= 0.15.0"
|
|
20
19
|
end
|