secure_headers 3.2.0 → 3.7.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 +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +2 -0
- data/CHANGELOG.md +140 -1
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +3 -1
- data/LICENSE +4 -199
- data/README.md +74 -334
- data/docs/HPKP.md +17 -0
- data/docs/cookies.md +50 -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 +89 -32
- data/lib/secure_headers/headers/content_security_policy_config.rb +161 -0
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
- data/lib/secure_headers/headers/policy_management.rb +84 -49
- 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/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 +69 -12
- data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +28 -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 -4
|
@@ -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
|
|
@@ -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)
|
data/lib/secure_headers.rb
CHANGED
|
@@ -9,24 +9,56 @@ require "secure_headers/headers/x_xss_protection"
|
|
|
9
9
|
require "secure_headers/headers/x_content_type_options"
|
|
10
10
|
require "secure_headers/headers/x_download_options"
|
|
11
11
|
require "secure_headers/headers/x_permitted_cross_domain_policies"
|
|
12
|
+
require "secure_headers/headers/referrer_policy"
|
|
13
|
+
require "secure_headers/headers/clear_site_data"
|
|
14
|
+
require "secure_headers/headers/expect_certificate_transparency"
|
|
12
15
|
require "secure_headers/middleware"
|
|
13
16
|
require "secure_headers/railtie"
|
|
14
17
|
require "secure_headers/view_helper"
|
|
15
18
|
require "useragent"
|
|
19
|
+
require "singleton"
|
|
16
20
|
|
|
17
21
|
# All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
|
|
18
22
|
# or ":optout_of_protection" as a config value to disable a given header
|
|
19
23
|
module SecureHeaders
|
|
20
|
-
|
|
24
|
+
class NoOpHeaderConfig
|
|
25
|
+
include Singleton
|
|
26
|
+
|
|
27
|
+
def boom(arg = nil)
|
|
28
|
+
raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead."
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_h
|
|
32
|
+
{}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def dup
|
|
36
|
+
self.class.instance
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def opt_out?
|
|
40
|
+
true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
alias_method :[], :boom
|
|
44
|
+
alias_method :[]=, :boom
|
|
45
|
+
alias_method :keys, :boom
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
OPT_OUT = NoOpHeaderConfig.instance
|
|
21
49
|
SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze
|
|
22
50
|
NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze
|
|
23
51
|
HTTPS = "https".freeze
|
|
24
52
|
CSP = ContentSecurityPolicy
|
|
25
53
|
|
|
26
54
|
ALL_HEADER_CLASSES = [
|
|
27
|
-
|
|
55
|
+
ExpectCertificateTransparency,
|
|
56
|
+
ClearSiteData,
|
|
57
|
+
ContentSecurityPolicyConfig,
|
|
58
|
+
ContentSecurityPolicyReportOnlyConfig,
|
|
28
59
|
StrictTransportSecurity,
|
|
29
60
|
PublicKeyPins,
|
|
61
|
+
ReferrerPolicy,
|
|
30
62
|
XContentTypeOptions,
|
|
31
63
|
XDownloadOptions,
|
|
32
64
|
XFrameOptions,
|
|
@@ -34,7 +66,10 @@ module SecureHeaders
|
|
|
34
66
|
XXssProtection
|
|
35
67
|
].freeze
|
|
36
68
|
|
|
37
|
-
ALL_HEADERS_BESIDES_CSP = (
|
|
69
|
+
ALL_HEADERS_BESIDES_CSP = (
|
|
70
|
+
ALL_HEADER_CLASSES -
|
|
71
|
+
[ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig]
|
|
72
|
+
).freeze
|
|
38
73
|
|
|
39
74
|
# Headers set on http requests (excludes STS and HPKP)
|
|
40
75
|
HTTP_HEADER_CLASSES =
|
|
@@ -48,13 +83,25 @@ module SecureHeaders
|
|
|
48
83
|
#
|
|
49
84
|
# additions - a hash containing directives. e.g.
|
|
50
85
|
# script_src: %w(another-host.com)
|
|
51
|
-
def override_content_security_policy_directives(request, additions)
|
|
52
|
-
config =
|
|
53
|
-
|
|
54
|
-
|
|
86
|
+
def override_content_security_policy_directives(request, additions, target = nil)
|
|
87
|
+
config, target = config_and_target(request, target)
|
|
88
|
+
|
|
89
|
+
if [:both, :enforced].include?(target)
|
|
90
|
+
if config.csp.opt_out?
|
|
91
|
+
config.csp = ContentSecurityPolicyConfig.new({})
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
config.csp.merge!(additions)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if [:both, :report_only].include?(target)
|
|
98
|
+
if config.csp_report_only.opt_out?
|
|
99
|
+
config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({})
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
config.csp_report_only.merge!(additions)
|
|
55
103
|
end
|
|
56
104
|
|
|
57
|
-
config.dynamic_csp = config.current_csp.merge(additions)
|
|
58
105
|
override_secure_headers_request_config(request, config)
|
|
59
106
|
end
|
|
60
107
|
|
|
@@ -64,12 +111,25 @@ module SecureHeaders
|
|
|
64
111
|
#
|
|
65
112
|
# additions - a hash containing directives. e.g.
|
|
66
113
|
# script_src: %w(another-host.com)
|
|
67
|
-
def append_content_security_policy_directives(request, additions)
|
|
68
|
-
config =
|
|
69
|
-
|
|
114
|
+
def append_content_security_policy_directives(request, additions, target = nil)
|
|
115
|
+
config, target = config_and_target(request, target)
|
|
116
|
+
|
|
117
|
+
if [:both, :enforced].include?(target) && !config.csp.opt_out?
|
|
118
|
+
config.csp.append(additions)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if [:both, :report_only].include?(target) && !config.csp_report_only.opt_out?
|
|
122
|
+
config.csp_report_only.append(additions)
|
|
123
|
+
end
|
|
124
|
+
|
|
70
125
|
override_secure_headers_request_config(request, config)
|
|
71
126
|
end
|
|
72
127
|
|
|
128
|
+
def use_content_security_policy_named_append(request, name)
|
|
129
|
+
additions = SecureHeaders::Configuration.named_appends(name).call(request)
|
|
130
|
+
append_content_security_policy_directives(request, additions)
|
|
131
|
+
end
|
|
132
|
+
|
|
73
133
|
# Public: override X-Frame-Options settings for this request.
|
|
74
134
|
#
|
|
75
135
|
# value - deny, sameorigin, or allowall
|
|
@@ -105,12 +165,29 @@ module SecureHeaders
|
|
|
105
165
|
# returned is meant to be merged into the header value from `@app.call(env)`
|
|
106
166
|
# in Rack middleware.
|
|
107
167
|
def header_hash_for(request)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
168
|
+
prevent_dup = true
|
|
169
|
+
config = config_for(request, prevent_dup)
|
|
170
|
+
headers = config.cached_headers
|
|
171
|
+
user_agent = UserAgent.parse(request.user_agent)
|
|
172
|
+
|
|
173
|
+
if !config.csp.opt_out? && config.csp.modified?
|
|
174
|
+
headers = update_cached_csp(config.csp, headers, user_agent)
|
|
111
175
|
end
|
|
112
176
|
|
|
113
|
-
|
|
177
|
+
if !config.csp_report_only.opt_out? && config.csp_report_only.modified?
|
|
178
|
+
headers = update_cached_csp(config.csp_report_only, headers, user_agent)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
header_classes_for(request).each_with_object({}) do |klass, hash|
|
|
182
|
+
if header = headers[klass::CONFIG_KEY]
|
|
183
|
+
header_name, value = if klass == ContentSecurityPolicyConfig || klass == ContentSecurityPolicyReportOnlyConfig
|
|
184
|
+
csp_header_for_ua(header, user_agent)
|
|
185
|
+
else
|
|
186
|
+
header
|
|
187
|
+
end
|
|
188
|
+
hash[header_name] = value
|
|
189
|
+
end
|
|
190
|
+
end
|
|
114
191
|
end
|
|
115
192
|
|
|
116
193
|
# Public: specify which named override will be used for this request.
|
|
@@ -131,7 +208,7 @@ module SecureHeaders
|
|
|
131
208
|
#
|
|
132
209
|
# Returns the nonce
|
|
133
210
|
def content_security_policy_script_nonce(request)
|
|
134
|
-
content_security_policy_nonce(request,
|
|
211
|
+
content_security_policy_nonce(request, ContentSecurityPolicy::SCRIPT_SRC)
|
|
135
212
|
end
|
|
136
213
|
|
|
137
214
|
# Public: gets or creates a nonce for CSP.
|
|
@@ -140,7 +217,7 @@ module SecureHeaders
|
|
|
140
217
|
#
|
|
141
218
|
# Returns the nonce
|
|
142
219
|
def content_security_policy_style_nonce(request)
|
|
143
|
-
content_security_policy_nonce(request,
|
|
220
|
+
content_security_policy_nonce(request, ContentSecurityPolicy::STYLE_SRC)
|
|
144
221
|
end
|
|
145
222
|
|
|
146
223
|
# Public: Retreives the config for a given header type:
|
|
@@ -148,11 +225,15 @@ module SecureHeaders
|
|
|
148
225
|
# Checks to see if there is an override for this request, then
|
|
149
226
|
# Checks to see if a named override is used for this request, then
|
|
150
227
|
# Falls back to the global config
|
|
151
|
-
def config_for(request)
|
|
228
|
+
def config_for(request, prevent_dup = false)
|
|
152
229
|
config = request.env[SECURE_HEADERS_CONFIG] ||
|
|
153
230
|
Configuration.get(Configuration::DEFAULT_CONFIG)
|
|
154
231
|
|
|
155
|
-
|
|
232
|
+
|
|
233
|
+
# Global configs are frozen, per-request configs are not. When we're not
|
|
234
|
+
# making modifications to the config, prevent_dup ensures we don't dup
|
|
235
|
+
# the object unnecessarily. It's not necessarily frozen to begin with.
|
|
236
|
+
if config.frozen? && !prevent_dup
|
|
156
237
|
config.dup
|
|
157
238
|
else
|
|
158
239
|
config
|
|
@@ -160,13 +241,38 @@ module SecureHeaders
|
|
|
160
241
|
end
|
|
161
242
|
|
|
162
243
|
private
|
|
244
|
+
TARGETS = [:both, :enforced, :report_only]
|
|
245
|
+
def raise_on_unknown_target(target)
|
|
246
|
+
unless TARGETS.include?(target)
|
|
247
|
+
raise "Unrecognized target: #{target}. Must be [:both, :enforced, :report_only]"
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def config_and_target(request, target)
|
|
252
|
+
config = config_for(request)
|
|
253
|
+
target = guess_target(config) unless target
|
|
254
|
+
raise_on_unknown_target(target)
|
|
255
|
+
[config, target]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def guess_target(config)
|
|
259
|
+
if !config.csp.opt_out? && !config.csp_report_only.opt_out?
|
|
260
|
+
:both
|
|
261
|
+
elsif !config.csp.opt_out?
|
|
262
|
+
:enforced
|
|
263
|
+
elsif !config.csp_report_only.opt_out?
|
|
264
|
+
:report_only
|
|
265
|
+
else
|
|
266
|
+
:both
|
|
267
|
+
end
|
|
268
|
+
end
|
|
163
269
|
|
|
164
270
|
# Private: gets or creates a nonce for CSP.
|
|
165
271
|
#
|
|
166
272
|
# Returns the nonce
|
|
167
273
|
def content_security_policy_nonce(request, script_or_style)
|
|
168
274
|
request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp
|
|
169
|
-
nonce_key = script_or_style ==
|
|
275
|
+
nonce_key = script_or_style == ContentSecurityPolicy::SCRIPT_SRC ? :script_nonce : :style_nonce
|
|
170
276
|
append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY])
|
|
171
277
|
request.env[NONCE_KEY]
|
|
172
278
|
end
|
|
@@ -191,21 +297,12 @@ module SecureHeaders
|
|
|
191
297
|
end
|
|
192
298
|
end
|
|
193
299
|
|
|
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
|
|
300
|
+
def update_cached_csp(config, headers, user_agent)
|
|
301
|
+
headers = Configuration.send(:deep_copy, headers)
|
|
302
|
+
headers[config.class::CONFIG_KEY] = {}
|
|
303
|
+
variation = ContentSecurityPolicy.ua_to_variation(user_agent)
|
|
304
|
+
headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent)
|
|
305
|
+
headers
|
|
209
306
|
end
|
|
210
307
|
|
|
211
308
|
# Private: chooses the applicable CSP header for the provided user agent.
|
|
@@ -213,26 +310,8 @@ module SecureHeaders
|
|
|
213
310
|
# headers - a hash of header_config_key => [header_name, header_value]
|
|
214
311
|
#
|
|
215
312
|
# 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
|
|
313
|
+
def csp_header_for_ua(headers, user_agent)
|
|
314
|
+
headers[ContentSecurityPolicy.ua_to_variation(user_agent)]
|
|
236
315
|
end
|
|
237
316
|
end
|
|
238
317
|
|
|
@@ -265,4 +344,8 @@ module SecureHeaders
|
|
|
265
344
|
def override_x_frame_options(value)
|
|
266
345
|
SecureHeaders.override_x_frame_options(request, value)
|
|
267
346
|
end
|
|
347
|
+
|
|
348
|
+
def use_content_security_policy_named_append(name)
|
|
349
|
+
SecureHeaders.use_content_security_policy_named_append(request, name)
|
|
350
|
+
end
|
|
268
351
|
end
|
data/lib/tasks/tasks.rake
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx
|
|
2
|
-
INLINE_STYLE_REGEX = /(<style[^>]*>)(.*?)<\/style>/mx
|
|
3
|
-
INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
|
|
4
|
-
INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
|
|
1
|
+
INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx unless defined? INLINE_SCRIPT_REGEX
|
|
2
|
+
INLINE_STYLE_REGEX = /(<style[^>]*>)(.*?)<\/style>/mx unless defined? INLINE_STYLE_REGEX
|
|
3
|
+
INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_SCRIPT_HELPER_REGEX
|
|
4
|
+
INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_STYLE_HELPER_REGEX
|
|
5
5
|
|
|
6
6
|
namespace :secure_headers do
|
|
7
7
|
include SecureHeaders::HashHelper
|
|
@@ -54,6 +54,7 @@ namespace :secure_headers do
|
|
|
54
54
|
hashes
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
desc "Generate #{SecureHeaders::Configuration::HASH_CONFIG_FILE}"
|
|
57
58
|
task :generate_hashes do |t, args|
|
|
58
59
|
script_hashes = {
|
|
59
60
|
"scripts" => {},
|
data/secure_headers.gemspec
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
2
|
Gem::Specification.new do |gem|
|
|
3
3
|
gem.name = "secure_headers"
|
|
4
|
-
gem.version = "3.
|
|
4
|
+
gem.version = "3.7.0"
|
|
5
5
|
gem.authors = ["Neil Matatall"]
|
|
6
6
|
gem.email = ["neil.matatall@gmail.com"]
|
|
7
|
-
gem.description = '
|
|
7
|
+
gem.description = 'Manages application of security headers with many safe defaults.'
|
|
8
8
|
gem.summary = 'Add easily configured security headers to responses
|
|
9
9
|
including content-security-policy, x-frame-options,
|
|
10
10
|
strict-transport-security, etc.'
|
|
@@ -36,8 +36,8 @@ module SecureHeaders
|
|
|
36
36
|
|
|
37
37
|
config = Configuration.get(:test_override)
|
|
38
38
|
noop = Configuration.get(Configuration::NOOP_CONFIGURATION)
|
|
39
|
-
[:csp, :
|
|
40
|
-
expect(config.send(key)).to eq(noop.send(key))
|
|
39
|
+
[:csp, :csp_report_only, :cookies].each do |key|
|
|
40
|
+
expect(config.send(key)).to eq(noop.send(key))
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
@@ -65,7 +65,7 @@ module SecureHeaders
|
|
|
65
65
|
default = Configuration.get
|
|
66
66
|
override = Configuration.get(:override)
|
|
67
67
|
|
|
68
|
-
expect(override.csp).not_to
|
|
68
|
+
expect(override.csp.directive_value(:default_src)).not_to be(default.csp.directive_value(:default_src))
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
it "allows you to override an override" do
|
|
@@ -78,9 +78,9 @@ module SecureHeaders
|
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
original_override = Configuration.get(:override)
|
|
81
|
-
expect(original_override.csp).to eq(default_src: %w('self'))
|
|
81
|
+
expect(original_override.csp.to_h).to eq(default_src: %w('self'))
|
|
82
82
|
override_config = Configuration.get(:second_override)
|
|
83
|
-
expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org))
|
|
83
|
+
expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org))
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
it "deprecates the secure_cookies configuration" do
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module SecureHeaders
|
|
4
|
+
describe ClearSiteData do
|
|
5
|
+
describe "make_header" do
|
|
6
|
+
it "returns nil with nil config" do
|
|
7
|
+
expect(described_class.make_header).to be_nil
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "returns nil with empty config" do
|
|
11
|
+
expect(described_class.make_header([])).to be_nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "returns nil with opt-out config" do
|
|
15
|
+
expect(described_class.make_header(OPT_OUT)).to be_nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "returns all types with `true` config" do
|
|
19
|
+
name, value = described_class.make_header(true)
|
|
20
|
+
|
|
21
|
+
expect(name).to eq(ClearSiteData::HEADER_NAME)
|
|
22
|
+
expect(value).to eq(
|
|
23
|
+
%("cache", "cookies", "storage", "executionContexts")
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "returns specified types" do
|
|
28
|
+
name, value = described_class.make_header(["foo", "bar"])
|
|
29
|
+
|
|
30
|
+
expect(name).to eq(ClearSiteData::HEADER_NAME)
|
|
31
|
+
expect(value).to eq(%("foo", "bar"))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe "validate_config!" do
|
|
36
|
+
it "succeeds for `true` config" do
|
|
37
|
+
expect do
|
|
38
|
+
described_class.validate_config!(true)
|
|
39
|
+
end.not_to raise_error
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "succeeds for `nil` config" do
|
|
43
|
+
expect do
|
|
44
|
+
described_class.validate_config!(nil)
|
|
45
|
+
end.not_to raise_error
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "succeeds for opt-out config" do
|
|
49
|
+
expect do
|
|
50
|
+
described_class.validate_config!(OPT_OUT)
|
|
51
|
+
end.not_to raise_error
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "succeeds for empty config" do
|
|
55
|
+
expect do
|
|
56
|
+
described_class.validate_config!([])
|
|
57
|
+
end.not_to raise_error
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "succeeds for Array of Strings config" do
|
|
61
|
+
expect do
|
|
62
|
+
described_class.validate_config!(["foo"])
|
|
63
|
+
end.not_to raise_error
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "fails for Array of non-String config" do
|
|
67
|
+
expect do
|
|
68
|
+
described_class.validate_config!([1])
|
|
69
|
+
end.to raise_error(ClearSiteDataConfigError)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "fails for other types of config" do
|
|
73
|
+
expect do
|
|
74
|
+
described_class.validate_config!(:cookies)
|
|
75
|
+
end.to raise_error(ClearSiteDataConfigError)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe "make_header_value" do
|
|
80
|
+
it "returns a string of quoted values that are comma separated" do
|
|
81
|
+
value = described_class.make_header_value(["foo", "bar"])
|
|
82
|
+
expect(value).to eq(%("foo", "bar"))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|