secure_headers 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE.md +41 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- data/.rspec +2 -0
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -1
- data/.travis.yml +8 -4
- data/CHANGELOG.md +144 -1
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +9 -4
- data/Guardfile +1 -0
- data/LICENSE +4 -199
- data/README.md +73 -344
- data/Rakefile +22 -18
- data/docs/HPKP.md +17 -0
- data/docs/cookies.md +64 -0
- data/docs/hashes.md +64 -0
- data/docs/named_overrides_and_appends.md +107 -0
- data/docs/per_action_configuration.md +133 -0
- data/docs/sinatra.md +25 -0
- data/lib/secure_headers/configuration.rb +116 -42
- data/lib/secure_headers/hash_helper.rb +2 -1
- data/lib/secure_headers/headers/clear_site_data.rb +55 -0
- data/lib/secure_headers/headers/content_security_policy.rb +128 -39
- data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
- data/lib/secure_headers/headers/cookie.rb +21 -3
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
- data/lib/secure_headers/headers/policy_management.rb +158 -68
- data/lib/secure_headers/headers/public_key_pins.rb +5 -4
- data/lib/secure_headers/headers/referrer_policy.rb +37 -0
- data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
- data/lib/secure_headers/headers/x_content_type_options.rb +2 -1
- data/lib/secure_headers/headers/x_download_options.rb +3 -2
- data/lib/secure_headers/headers/x_frame_options.rb +2 -1
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
- data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
- data/lib/secure_headers/middleware.rb +16 -9
- data/lib/secure_headers/railtie.rb +7 -6
- data/lib/secure_headers/utils/cookies_config.rb +13 -12
- data/lib/secure_headers/view_helper.rb +15 -8
- data/lib/secure_headers.rb +139 -55
- data/lib/tasks/tasks.rake +7 -5
- data/secure_headers.gemspec +13 -3
- data/spec/lib/secure_headers/configuration_spec.rb +13 -12
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +89 -15
- data/spec/lib/secure_headers/headers/cookie_spec.rb +38 -20
- data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +80 -42
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +73 -0
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +5 -4
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
- data/spec/lib/secure_headers/middleware_spec.rb +55 -17
- data/spec/lib/secure_headers/view_helpers_spec.rb +21 -8
- data/spec/lib/secure_headers_spec.rb +330 -58
- data/spec/spec_helper.rb +17 -25
- data/upgrading-to-3-0.md +13 -10
- data/upgrading-to-4-0.md +55 -0
- metadata +34 -7
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module SecureHeaders
|
|
2
3
|
class CookiesConfig
|
|
3
4
|
|
|
@@ -11,9 +12,9 @@ module SecureHeaders
|
|
|
11
12
|
return if config.nil? || config == SecureHeaders::OPT_OUT
|
|
12
13
|
|
|
13
14
|
validate_config!
|
|
14
|
-
validate_secure_config!
|
|
15
|
-
validate_httponly_config!
|
|
16
|
-
validate_samesite_config!
|
|
15
|
+
validate_secure_config! unless config[:secure].nil?
|
|
16
|
+
validate_httponly_config! unless config[:httponly].nil?
|
|
17
|
+
validate_samesite_config! unless config[:samesite].nil?
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
private
|
|
@@ -23,16 +24,17 @@ module SecureHeaders
|
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def validate_secure_config!
|
|
26
|
-
|
|
27
|
+
validate_hash_or_true_or_opt_out!(:secure)
|
|
27
28
|
validate_exclusive_use_of_hash_constraints!(config[:secure], :secure)
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
def validate_httponly_config!
|
|
31
|
-
|
|
32
|
+
validate_hash_or_true_or_opt_out!(:httponly)
|
|
32
33
|
validate_exclusive_use_of_hash_constraints!(config[:httponly], :httponly)
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def validate_samesite_config!
|
|
37
|
+
return if config[:samesite] == OPT_OUT
|
|
36
38
|
raise CookiesConfigError.new("samesite cookie config must be a hash") unless is_hash?(config[:samesite])
|
|
37
39
|
|
|
38
40
|
validate_samesite_boolean_config!
|
|
@@ -51,18 +53,18 @@ module SecureHeaders
|
|
|
51
53
|
def validate_samesite_hash_config!
|
|
52
54
|
# validate Hash-based samesite configuration
|
|
53
55
|
if is_hash?(config[:samesite][:lax])
|
|
54
|
-
validate_exclusive_use_of_hash_constraints!(config[:samesite][:lax],
|
|
56
|
+
validate_exclusive_use_of_hash_constraints!(config[:samesite][:lax], "samesite lax")
|
|
55
57
|
|
|
56
58
|
if is_hash?(config[:samesite][:strict])
|
|
57
|
-
validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict],
|
|
59
|
+
validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict], "samesite strict")
|
|
58
60
|
validate_exclusive_use_of_samesite_enforcement!(:only)
|
|
59
61
|
validate_exclusive_use_of_samesite_enforcement!(:except)
|
|
60
62
|
end
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
|
|
64
|
-
def
|
|
65
|
-
if !(is_hash?(config[attribute]) ||
|
|
66
|
+
def validate_hash_or_true_or_opt_out!(attribute)
|
|
67
|
+
if !(is_hash?(config[attribute]) || is_true_or_opt_out?(config[attribute]))
|
|
66
68
|
raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean")
|
|
67
69
|
end
|
|
68
70
|
end
|
|
@@ -70,7 +72,6 @@ module SecureHeaders
|
|
|
70
72
|
# validate exclusive use of only or except but not both at the same time
|
|
71
73
|
def validate_exclusive_use_of_hash_constraints!(conf, attribute)
|
|
72
74
|
return unless is_hash?(conf)
|
|
73
|
-
|
|
74
75
|
if conf.key?(:only) && conf.key?(:except)
|
|
75
76
|
raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.")
|
|
76
77
|
end
|
|
@@ -87,8 +88,8 @@ module SecureHeaders
|
|
|
87
88
|
obj && obj.is_a?(Hash)
|
|
88
89
|
end
|
|
89
90
|
|
|
90
|
-
def
|
|
91
|
-
obj && (obj.is_a?(TrueClass) || obj
|
|
91
|
+
def is_true_or_opt_out?(obj)
|
|
92
|
+
obj && (obj.is_a?(TrueClass) || obj == OPT_OUT)
|
|
92
93
|
end
|
|
93
94
|
end
|
|
94
95
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module SecureHeaders
|
|
2
3
|
module ViewHelpers
|
|
3
4
|
include SecureHeaders::HashHelper
|
|
@@ -34,6 +35,14 @@ module SecureHeaders
|
|
|
34
35
|
end
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
def content_security_policy_script_nonce
|
|
39
|
+
content_security_policy_nonce(:script)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def content_security_policy_style_nonce
|
|
43
|
+
content_security_policy_nonce(:style)
|
|
44
|
+
end
|
|
45
|
+
|
|
37
46
|
##
|
|
38
47
|
# Checks to see if the hashed code is expected and adds the hash source
|
|
39
48
|
# value to the current CSP.
|
|
@@ -67,7 +76,7 @@ module SecureHeaders
|
|
|
67
76
|
end
|
|
68
77
|
|
|
69
78
|
content = capture(&block)
|
|
70
|
-
file_path = File.join(
|
|
79
|
+
file_path = File.join("app", "views", self.instance_variable_get(:@virtual_path) + ".html.erb")
|
|
71
80
|
|
|
72
81
|
if raise_error_on_unrecognized_hash
|
|
73
82
|
hash_value = hash_source(content)
|
|
@@ -87,9 +96,9 @@ module SecureHeaders
|
|
|
87
96
|
<<-EOF
|
|
88
97
|
\n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
|
|
89
98
|
#{content}
|
|
90
|
-
*** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/
|
|
99
|
+
*** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/secure_headers_generated_hashes.yml:***
|
|
91
100
|
#{file_path}:
|
|
92
|
-
- #{hash_value}\n\n
|
|
101
|
+
- \"#{hash_value}\"\n\n
|
|
93
102
|
NOTE: dynamic javascript is not supported using script hash integration
|
|
94
103
|
on purpose. It defeats the point of using it in the first place.
|
|
95
104
|
EOF
|
|
@@ -108,8 +117,6 @@ module SecureHeaders
|
|
|
108
117
|
end
|
|
109
118
|
end
|
|
110
119
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
end
|
|
115
|
-
end
|
|
120
|
+
ActiveSupport.on_load :action_view do
|
|
121
|
+
include SecureHeaders::ViewHelpers
|
|
122
|
+
end if defined?(ActiveSupport)
|
data/lib/secure_headers.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require "secure_headers/configuration"
|
|
2
3
|
require "secure_headers/hash_helper"
|
|
3
4
|
require "secure_headers/headers/cookie"
|
|
@@ -9,24 +10,56 @@ require "secure_headers/headers/x_xss_protection"
|
|
|
9
10
|
require "secure_headers/headers/x_content_type_options"
|
|
10
11
|
require "secure_headers/headers/x_download_options"
|
|
11
12
|
require "secure_headers/headers/x_permitted_cross_domain_policies"
|
|
13
|
+
require "secure_headers/headers/referrer_policy"
|
|
14
|
+
require "secure_headers/headers/clear_site_data"
|
|
15
|
+
require "secure_headers/headers/expect_certificate_transparency"
|
|
12
16
|
require "secure_headers/middleware"
|
|
13
17
|
require "secure_headers/railtie"
|
|
14
18
|
require "secure_headers/view_helper"
|
|
15
19
|
require "useragent"
|
|
20
|
+
require "singleton"
|
|
16
21
|
|
|
17
22
|
# All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
|
|
18
23
|
# or ":optout_of_protection" as a config value to disable a given header
|
|
19
24
|
module SecureHeaders
|
|
20
|
-
|
|
25
|
+
class NoOpHeaderConfig
|
|
26
|
+
include Singleton
|
|
27
|
+
|
|
28
|
+
def boom(*args)
|
|
29
|
+
raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead."
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_h
|
|
33
|
+
{}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def dup
|
|
37
|
+
self.class.instance
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def opt_out?
|
|
41
|
+
true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
alias_method :[], :boom
|
|
45
|
+
alias_method :[]=, :boom
|
|
46
|
+
alias_method :keys, :boom
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
OPT_OUT = NoOpHeaderConfig.instance
|
|
21
50
|
SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze
|
|
22
51
|
NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze
|
|
23
52
|
HTTPS = "https".freeze
|
|
24
53
|
CSP = ContentSecurityPolicy
|
|
25
54
|
|
|
26
55
|
ALL_HEADER_CLASSES = [
|
|
27
|
-
|
|
56
|
+
ExpectCertificateTransparency,
|
|
57
|
+
ClearSiteData,
|
|
58
|
+
ContentSecurityPolicyConfig,
|
|
59
|
+
ContentSecurityPolicyReportOnlyConfig,
|
|
28
60
|
StrictTransportSecurity,
|
|
29
61
|
PublicKeyPins,
|
|
62
|
+
ReferrerPolicy,
|
|
30
63
|
XContentTypeOptions,
|
|
31
64
|
XDownloadOptions,
|
|
32
65
|
XFrameOptions,
|
|
@@ -34,7 +67,10 @@ module SecureHeaders
|
|
|
34
67
|
XXssProtection
|
|
35
68
|
].freeze
|
|
36
69
|
|
|
37
|
-
ALL_HEADERS_BESIDES_CSP = (
|
|
70
|
+
ALL_HEADERS_BESIDES_CSP = (
|
|
71
|
+
ALL_HEADER_CLASSES -
|
|
72
|
+
[ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig]
|
|
73
|
+
).freeze
|
|
38
74
|
|
|
39
75
|
# Headers set on http requests (excludes STS and HPKP)
|
|
40
76
|
HTTP_HEADER_CLASSES =
|
|
@@ -48,13 +84,25 @@ module SecureHeaders
|
|
|
48
84
|
#
|
|
49
85
|
# additions - a hash containing directives. e.g.
|
|
50
86
|
# script_src: %w(another-host.com)
|
|
51
|
-
def override_content_security_policy_directives(request, additions)
|
|
52
|
-
config =
|
|
53
|
-
|
|
54
|
-
|
|
87
|
+
def override_content_security_policy_directives(request, additions, target = nil)
|
|
88
|
+
config, target = config_and_target(request, target)
|
|
89
|
+
|
|
90
|
+
if [:both, :enforced].include?(target)
|
|
91
|
+
if config.csp.opt_out?
|
|
92
|
+
config.csp = ContentSecurityPolicyConfig.new({})
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
config.csp.merge!(additions)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if [:both, :report_only].include?(target)
|
|
99
|
+
if config.csp_report_only.opt_out?
|
|
100
|
+
config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({})
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
config.csp_report_only.merge!(additions)
|
|
55
104
|
end
|
|
56
105
|
|
|
57
|
-
config.dynamic_csp = config.current_csp.merge(additions)
|
|
58
106
|
override_secure_headers_request_config(request, config)
|
|
59
107
|
end
|
|
60
108
|
|
|
@@ -64,12 +112,25 @@ module SecureHeaders
|
|
|
64
112
|
#
|
|
65
113
|
# additions - a hash containing directives. e.g.
|
|
66
114
|
# script_src: %w(another-host.com)
|
|
67
|
-
def append_content_security_policy_directives(request, additions)
|
|
68
|
-
config =
|
|
69
|
-
|
|
115
|
+
def append_content_security_policy_directives(request, additions, target = nil)
|
|
116
|
+
config, target = config_and_target(request, target)
|
|
117
|
+
|
|
118
|
+
if [:both, :enforced].include?(target) && !config.csp.opt_out?
|
|
119
|
+
config.csp.append(additions)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if [:both, :report_only].include?(target) && !config.csp_report_only.opt_out?
|
|
123
|
+
config.csp_report_only.append(additions)
|
|
124
|
+
end
|
|
125
|
+
|
|
70
126
|
override_secure_headers_request_config(request, config)
|
|
71
127
|
end
|
|
72
128
|
|
|
129
|
+
def use_content_security_policy_named_append(request, name)
|
|
130
|
+
additions = SecureHeaders::Configuration.named_appends(name).call(request)
|
|
131
|
+
append_content_security_policy_directives(request, additions)
|
|
132
|
+
end
|
|
133
|
+
|
|
73
134
|
# Public: override X-Frame-Options settings for this request.
|
|
74
135
|
#
|
|
75
136
|
# value - deny, sameorigin, or allowall
|
|
@@ -105,12 +166,29 @@ module SecureHeaders
|
|
|
105
166
|
# returned is meant to be merged into the header value from `@app.call(env)`
|
|
106
167
|
# in Rack middleware.
|
|
107
168
|
def header_hash_for(request)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
169
|
+
prevent_dup = true
|
|
170
|
+
config = config_for(request, prevent_dup)
|
|
171
|
+
headers = config.cached_headers
|
|
172
|
+
user_agent = UserAgent.parse(request.user_agent)
|
|
173
|
+
|
|
174
|
+
if !config.csp.opt_out? && config.csp.modified?
|
|
175
|
+
headers = update_cached_csp(config.csp, headers, user_agent)
|
|
111
176
|
end
|
|
112
177
|
|
|
113
|
-
|
|
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
|
|
190
|
+
end
|
|
191
|
+
end
|
|
114
192
|
end
|
|
115
193
|
|
|
116
194
|
# Public: specify which named override will be used for this request.
|
|
@@ -131,7 +209,7 @@ module SecureHeaders
|
|
|
131
209
|
#
|
|
132
210
|
# Returns the nonce
|
|
133
211
|
def content_security_policy_script_nonce(request)
|
|
134
|
-
content_security_policy_nonce(request,
|
|
212
|
+
content_security_policy_nonce(request, ContentSecurityPolicy::SCRIPT_SRC)
|
|
135
213
|
end
|
|
136
214
|
|
|
137
215
|
# Public: gets or creates a nonce for CSP.
|
|
@@ -140,7 +218,7 @@ module SecureHeaders
|
|
|
140
218
|
#
|
|
141
219
|
# Returns the nonce
|
|
142
220
|
def content_security_policy_style_nonce(request)
|
|
143
|
-
content_security_policy_nonce(request,
|
|
221
|
+
content_security_policy_nonce(request, ContentSecurityPolicy::STYLE_SRC)
|
|
144
222
|
end
|
|
145
223
|
|
|
146
224
|
# Public: Retreives the config for a given header type:
|
|
@@ -148,11 +226,15 @@ module SecureHeaders
|
|
|
148
226
|
# Checks to see if there is an override for this request, then
|
|
149
227
|
# Checks to see if a named override is used for this request, then
|
|
150
228
|
# Falls back to the global config
|
|
151
|
-
def config_for(request)
|
|
229
|
+
def config_for(request, prevent_dup = false)
|
|
152
230
|
config = request.env[SECURE_HEADERS_CONFIG] ||
|
|
153
231
|
Configuration.get(Configuration::DEFAULT_CONFIG)
|
|
154
232
|
|
|
155
|
-
|
|
233
|
+
|
|
234
|
+
# Global configs are frozen, per-request configs are not. When we're not
|
|
235
|
+
# making modifications to the config, prevent_dup ensures we don't dup
|
|
236
|
+
# the object unnecessarily. It's not necessarily frozen to begin with.
|
|
237
|
+
if config.frozen? && !prevent_dup
|
|
156
238
|
config.dup
|
|
157
239
|
else
|
|
158
240
|
config
|
|
@@ -160,13 +242,38 @@ module SecureHeaders
|
|
|
160
242
|
end
|
|
161
243
|
|
|
162
244
|
private
|
|
245
|
+
TARGETS = [:both, :enforced, :report_only]
|
|
246
|
+
def raise_on_unknown_target(target)
|
|
247
|
+
unless TARGETS.include?(target)
|
|
248
|
+
raise "Unrecognized target: #{target}. Must be [:both, :enforced, :report_only]"
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def config_and_target(request, target)
|
|
253
|
+
config = config_for(request)
|
|
254
|
+
target = guess_target(config) unless target
|
|
255
|
+
raise_on_unknown_target(target)
|
|
256
|
+
[config, target]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def guess_target(config)
|
|
260
|
+
if !config.csp.opt_out? && !config.csp_report_only.opt_out?
|
|
261
|
+
:both
|
|
262
|
+
elsif !config.csp.opt_out?
|
|
263
|
+
:enforced
|
|
264
|
+
elsif !config.csp_report_only.opt_out?
|
|
265
|
+
:report_only
|
|
266
|
+
else
|
|
267
|
+
:both
|
|
268
|
+
end
|
|
269
|
+
end
|
|
163
270
|
|
|
164
271
|
# Private: gets or creates a nonce for CSP.
|
|
165
272
|
#
|
|
166
273
|
# Returns the nonce
|
|
167
274
|
def content_security_policy_nonce(request, script_or_style)
|
|
168
275
|
request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp
|
|
169
|
-
nonce_key = script_or_style ==
|
|
276
|
+
nonce_key = script_or_style == ContentSecurityPolicy::SCRIPT_SRC ? :script_nonce : :style_nonce
|
|
170
277
|
append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY])
|
|
171
278
|
request.env[NONCE_KEY]
|
|
172
279
|
end
|
|
@@ -191,21 +298,12 @@ module SecureHeaders
|
|
|
191
298
|
end
|
|
192
299
|
end
|
|
193
300
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if header = headers[klass::CONFIG_KEY]
|
|
201
|
-
header_name, value = if klass == CSP
|
|
202
|
-
csp_header_for_ua(header, request)
|
|
203
|
-
else
|
|
204
|
-
header
|
|
205
|
-
end
|
|
206
|
-
hash[header_name] = value
|
|
207
|
-
end
|
|
208
|
-
end
|
|
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
|
|
209
307
|
end
|
|
210
308
|
|
|
211
309
|
# Private: chooses the applicable CSP header for the provided user agent.
|
|
@@ -213,26 +311,8 @@ module SecureHeaders
|
|
|
213
311
|
# headers - a hash of header_config_key => [header_name, header_value]
|
|
214
312
|
#
|
|
215
313
|
# Returns a CSP [header, value] array
|
|
216
|
-
def csp_header_for_ua(headers,
|
|
217
|
-
headers[
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
# Private: optionally build a header with a given configure
|
|
221
|
-
#
|
|
222
|
-
# klass - corresponding Class for a given header
|
|
223
|
-
# config - A string, symbol, or hash config for the header
|
|
224
|
-
# user_agent - A string representing the UA (only used for CSP feature sniffing)
|
|
225
|
-
#
|
|
226
|
-
# Returns a 2 element array [header_name, header_value] or nil if config
|
|
227
|
-
# is OPT_OUT
|
|
228
|
-
def make_header(klass, header_config, user_agent = nil)
|
|
229
|
-
unless header_config == OPT_OUT
|
|
230
|
-
if klass == CSP
|
|
231
|
-
klass.make_header(header_config, user_agent)
|
|
232
|
-
else
|
|
233
|
-
klass.make_header(header_config)
|
|
234
|
-
end
|
|
235
|
-
end
|
|
314
|
+
def csp_header_for_ua(headers, user_agent)
|
|
315
|
+
headers[ContentSecurityPolicy.ua_to_variation(user_agent)]
|
|
236
316
|
end
|
|
237
317
|
end
|
|
238
318
|
|
|
@@ -265,4 +345,8 @@ module SecureHeaders
|
|
|
265
345
|
def override_x_frame_options(value)
|
|
266
346
|
SecureHeaders.override_x_frame_options(request, value)
|
|
267
347
|
end
|
|
348
|
+
|
|
349
|
+
def use_content_security_policy_named_append(name)
|
|
350
|
+
SecureHeaders.use_content_security_policy_named_append(request, name)
|
|
351
|
+
end
|
|
268
352
|
end
|
data/lib/tasks/tasks.rake
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx unless defined? INLINE_SCRIPT_REGEX
|
|
3
|
+
INLINE_STYLE_REGEX = /(<style[^>]*>)(.*?)<\/style>/mx unless defined? INLINE_STYLE_REGEX
|
|
4
|
+
INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_SCRIPT_HELPER_REGEX
|
|
5
|
+
INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_STYLE_HELPER_REGEX
|
|
5
6
|
|
|
6
7
|
namespace :secure_headers do
|
|
7
8
|
include SecureHeaders::HashHelper
|
|
@@ -54,6 +55,7 @@ namespace :secure_headers do
|
|
|
54
55
|
hashes
|
|
55
56
|
end
|
|
56
57
|
|
|
58
|
+
desc "Generate #{SecureHeaders::Configuration::HASH_CONFIG_FILE}"
|
|
57
59
|
task :generate_hashes do |t, args|
|
|
58
60
|
script_hashes = {
|
|
59
61
|
"scripts" => {},
|
|
@@ -72,7 +74,7 @@ namespace :secure_headers do
|
|
|
72
74
|
end
|
|
73
75
|
end
|
|
74
76
|
|
|
75
|
-
File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE,
|
|
77
|
+
File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE, "w") do |file|
|
|
76
78
|
file.write(script_hashes.to_yaml)
|
|
77
79
|
end
|
|
78
80
|
|
data/secure_headers.gemspec
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
Gem::Specification.new do |gem|
|
|
3
4
|
gem.name = "secure_headers"
|
|
4
|
-
gem.version = "
|
|
5
|
+
gem.version = "4.0.0"
|
|
5
6
|
gem.authors = ["Neil Matatall"]
|
|
6
7
|
gem.email = ["neil.matatall@gmail.com"]
|
|
7
|
-
gem.description =
|
|
8
|
+
gem.description = "Manages application of security headers with many safe defaults."
|
|
8
9
|
gem.summary = 'Add easily configured security headers to responses
|
|
9
10
|
including content-security-policy, x-frame-options,
|
|
10
11
|
strict-transport-security, etc.'
|
|
@@ -15,5 +16,14 @@ Gem::Specification.new do |gem|
|
|
|
15
16
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
16
17
|
gem.require_paths = ["lib"]
|
|
17
18
|
gem.add_development_dependency "rake"
|
|
18
|
-
gem.add_dependency "useragent"
|
|
19
|
+
gem.add_dependency "useragent", ">= 0.15.0"
|
|
20
|
+
|
|
21
|
+
# TODO: delete this after 4.1 is cut or a number of 4.0.x releases have occurred
|
|
22
|
+
gem.post_install_message = <<-POST_INSTALL
|
|
23
|
+
|
|
24
|
+
**********
|
|
25
|
+
:wave: secure_headers 4.0 introduces a lot of breaking changes (in the name of security!). It's highly likely you will need to update your secure_headers cookie configuration to avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/upgrading-to-4-0.md
|
|
26
|
+
**********
|
|
27
|
+
|
|
28
|
+
POST_INSTALL
|
|
19
29
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
2
3
|
|
|
3
4
|
module SecureHeaders
|
|
4
5
|
describe Configuration do
|
|
@@ -36,8 +37,8 @@ module SecureHeaders
|
|
|
36
37
|
|
|
37
38
|
config = Configuration.get(:test_override)
|
|
38
39
|
noop = Configuration.get(Configuration::NOOP_CONFIGURATION)
|
|
39
|
-
[:csp, :
|
|
40
|
-
expect(config.send(key)).to eq(noop.send(key))
|
|
40
|
+
[:csp, :csp_report_only, :cookies].each do |key|
|
|
41
|
+
expect(config.send(key)).to eq(noop.send(key))
|
|
41
42
|
end
|
|
42
43
|
end
|
|
43
44
|
|
|
@@ -65,12 +66,12 @@ module SecureHeaders
|
|
|
65
66
|
default = Configuration.get
|
|
66
67
|
override = Configuration.get(:override)
|
|
67
68
|
|
|
68
|
-
expect(override.csp).not_to
|
|
69
|
+
expect(override.csp.directive_value(:default_src)).not_to be(default.csp.directive_value(:default_src))
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
it "allows you to override an override" do
|
|
72
73
|
Configuration.override(:override) do |config|
|
|
73
|
-
config.csp = { default_src: %w('self')}
|
|
74
|
+
config.csp = { default_src: %w('self'), script_src: %w('self')}
|
|
74
75
|
end
|
|
75
76
|
|
|
76
77
|
Configuration.override(:second_override, :override) do |config|
|
|
@@ -78,17 +79,17 @@ module SecureHeaders
|
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
original_override = Configuration.get(:override)
|
|
81
|
-
expect(original_override.csp).to eq(default_src: %w('self'))
|
|
82
|
+
expect(original_override.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self'))
|
|
82
83
|
override_config = Configuration.get(:second_override)
|
|
83
|
-
expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org))
|
|
84
|
+
expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org))
|
|
84
85
|
end
|
|
85
86
|
|
|
86
87
|
it "deprecates the secure_cookies configuration" do
|
|
87
|
-
expect
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
expect {
|
|
89
|
+
Configuration.default do |config|
|
|
90
|
+
config.secure_cookies = true
|
|
91
|
+
end
|
|
92
|
+
}.to raise_error(ArgumentError)
|
|
92
93
|
end
|
|
93
94
|
end
|
|
94
95
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
3
|
+
|
|
4
|
+
module SecureHeaders
|
|
5
|
+
describe ClearSiteData do
|
|
6
|
+
describe "make_header" do
|
|
7
|
+
it "returns nil with nil config" do
|
|
8
|
+
expect(described_class.make_header).to be_nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "returns nil with empty config" do
|
|
12
|
+
expect(described_class.make_header([])).to be_nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "returns nil with opt-out config" do
|
|
16
|
+
expect(described_class.make_header(OPT_OUT)).to be_nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "returns all types with `true` config" do
|
|
20
|
+
name, value = described_class.make_header(true)
|
|
21
|
+
|
|
22
|
+
expect(name).to eq(ClearSiteData::HEADER_NAME)
|
|
23
|
+
expect(value).to eq(
|
|
24
|
+
%("cache", "cookies", "storage", "executionContexts")
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "returns specified types" do
|
|
29
|
+
name, value = described_class.make_header(["foo", "bar"])
|
|
30
|
+
|
|
31
|
+
expect(name).to eq(ClearSiteData::HEADER_NAME)
|
|
32
|
+
expect(value).to eq(%("foo", "bar"))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe "validate_config!" do
|
|
37
|
+
it "succeeds for `true` config" do
|
|
38
|
+
expect do
|
|
39
|
+
described_class.validate_config!(true)
|
|
40
|
+
end.not_to raise_error
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "succeeds for `nil` config" do
|
|
44
|
+
expect do
|
|
45
|
+
described_class.validate_config!(nil)
|
|
46
|
+
end.not_to raise_error
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "succeeds for opt-out config" do
|
|
50
|
+
expect do
|
|
51
|
+
described_class.validate_config!(OPT_OUT)
|
|
52
|
+
end.not_to raise_error
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "succeeds for empty config" do
|
|
56
|
+
expect do
|
|
57
|
+
described_class.validate_config!([])
|
|
58
|
+
end.not_to raise_error
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "succeeds for Array of Strings config" do
|
|
62
|
+
expect do
|
|
63
|
+
described_class.validate_config!(["foo"])
|
|
64
|
+
end.not_to raise_error
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "fails for Array of non-String config" do
|
|
68
|
+
expect do
|
|
69
|
+
described_class.validate_config!([1])
|
|
70
|
+
end.to raise_error(ClearSiteDataConfigError)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "fails for other types of config" do
|
|
74
|
+
expect do
|
|
75
|
+
described_class.validate_config!(:cookies)
|
|
76
|
+
end.to raise_error(ClearSiteDataConfigError)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe "make_header_value" do
|
|
81
|
+
it "returns a string of quoted values that are comma separated" do
|
|
82
|
+
value = described_class.make_header_value(["foo", "bar"])
|
|
83
|
+
expect(value).to eq(%("foo", "bar"))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|