secure_headers 3.4.0 → 3.5.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/.ruby-version +1 -1
- data/CHANGELOG.md +66 -0
- data/Gemfile +1 -0
- data/README.md +89 -9
- data/lib/secure_headers/configuration.rb +68 -36
- data/lib/secure_headers/headers/content_security_policy.rb +62 -36
- data/lib/secure_headers/headers/content_security_policy_config.rb +128 -0
- data/lib/secure_headers/headers/policy_management.rb +48 -30
- data/lib/secure_headers/view_helper.rb +8 -0
- data/lib/secure_headers.rb +131 -55
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/configuration_spec.rb +5 -5
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +2 -2
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +25 -34
- data/spec/lib/secure_headers/middleware_spec.rb +13 -1
- data/spec/lib/secure_headers/view_helpers_spec.rb +19 -6
- data/spec/lib/secure_headers_spec.rb +281 -40
- data/spec/spec_helper.rb +3 -2
- metadata +3 -2
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
module DynamicConfig
|
|
3
|
+
def self.included(base)
|
|
4
|
+
base.send(:attr_writer, :modified)
|
|
5
|
+
base.send(:attr_reader, *base.attrs)
|
|
6
|
+
base.attrs.each do |attr|
|
|
7
|
+
base.send(:define_method, "#{attr}=") do |value|
|
|
8
|
+
if self.class.attrs.include?(attr)
|
|
9
|
+
value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
|
|
10
|
+
prev_value = self.instance_variable_get("@#{attr}")
|
|
11
|
+
self.instance_variable_set("@#{attr}", value)
|
|
12
|
+
if prev_value != self.instance_variable_get("@#{attr}")
|
|
13
|
+
@modified = true
|
|
14
|
+
end
|
|
15
|
+
else
|
|
16
|
+
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(hash)
|
|
23
|
+
from_hash(hash)
|
|
24
|
+
@modified = false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def update_directive(directive, value)
|
|
28
|
+
self.send("#{directive}=", value)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def directive_value(directive)
|
|
32
|
+
if self.class.attrs.include?(directive)
|
|
33
|
+
self.send(directive)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def modified?
|
|
38
|
+
@modified
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def merge(new_hash)
|
|
42
|
+
ContentSecurityPolicy.combine_policies(self.to_h, new_hash)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def merge!(new_hash)
|
|
46
|
+
from_hash(new_hash)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def append(new_hash)
|
|
50
|
+
from_hash(ContentSecurityPolicy.combine_policies(self.to_h, new_hash))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_h
|
|
54
|
+
self.class.attrs.each_with_object({}) do |key, hash|
|
|
55
|
+
hash[key] = self.send(key)
|
|
56
|
+
end.reject { |_, v| v.nil? }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def dup
|
|
60
|
+
self.class.new(self.to_h)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def opt_out?
|
|
64
|
+
false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def ==(o)
|
|
68
|
+
self.class == o.class && self.to_h == o.to_h
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
alias_method :[], :directive_value
|
|
72
|
+
alias_method :[]=, :update_directive
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
def from_hash(hash)
|
|
76
|
+
hash.keys.reject { |k| hash[k].nil? }.map do |k|
|
|
77
|
+
if self.class.attrs.include?(k)
|
|
78
|
+
self.send("#{k}=", hash[k])
|
|
79
|
+
else
|
|
80
|
+
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{hash[k]}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class ContentSecurityPolicyConfigError < StandardError; end
|
|
87
|
+
class ContentSecurityPolicyConfig
|
|
88
|
+
CONFIG_KEY = :csp
|
|
89
|
+
HEADER_NAME = "Content-Security-Policy".freeze
|
|
90
|
+
|
|
91
|
+
def self.attrs
|
|
92
|
+
PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
include DynamicConfig
|
|
96
|
+
|
|
97
|
+
# based on what was suggested in https://github.com/rails/rails/pull/24961/files
|
|
98
|
+
DEFAULT = {
|
|
99
|
+
default_src: %w('self' https:),
|
|
100
|
+
font_src: %w('self' https: data:),
|
|
101
|
+
img_src: %w('self' https: data:),
|
|
102
|
+
object_src: %w('none'),
|
|
103
|
+
script_src: %w(https:),
|
|
104
|
+
style_src: %w('self' https: 'unsafe-inline')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
def report_only?
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def make_report_only
|
|
112
|
+
ContentSecurityPolicyReportOnlyConfig.new(self.to_h)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig
|
|
117
|
+
CONFIG_KEY = :csp_report_only
|
|
118
|
+
HEADER_NAME = "Content-Security-Policy-Report-Only".freeze
|
|
119
|
+
|
|
120
|
+
def report_only?
|
|
121
|
+
true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def make_report_only
|
|
125
|
+
self
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -7,9 +7,6 @@ module SecureHeaders
|
|
|
7
7
|
MODERN_BROWSERS = %w(Chrome Opera Firefox)
|
|
8
8
|
DEFAULT_VALUE = "default-src https:".freeze
|
|
9
9
|
DEFAULT_CONFIG = { default_src: %w(https:) }.freeze
|
|
10
|
-
HEADER_NAME = "Content-Security-Policy".freeze
|
|
11
|
-
REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
|
|
12
|
-
HEADER_NAMES = [HEADER_NAME, REPORT_ONLY]
|
|
13
10
|
DATA_PROTOCOL = "data:".freeze
|
|
14
11
|
BLOB_PROTOCOL = "blob:".freeze
|
|
15
12
|
SELF = "'self'".freeze
|
|
@@ -53,16 +50,6 @@ module SecureHeaders
|
|
|
53
50
|
FRAME_ANCESTORS = :frame_ancestors
|
|
54
51
|
PLUGIN_TYPES = :plugin_types
|
|
55
52
|
|
|
56
|
-
# These are directives that do not inherit the default-src value. This is
|
|
57
|
-
# useful when calling #combine_policies.
|
|
58
|
-
NON_FETCH_SOURCES = [
|
|
59
|
-
BASE_URI,
|
|
60
|
-
FORM_ACTION,
|
|
61
|
-
FRAME_ANCESTORS,
|
|
62
|
-
PLUGIN_TYPES,
|
|
63
|
-
REPORT_URI
|
|
64
|
-
]
|
|
65
|
-
|
|
66
53
|
DIRECTIVES_2_0 = [
|
|
67
54
|
DIRECTIVES_1_0,
|
|
68
55
|
BASE_URI,
|
|
@@ -127,6 +114,18 @@ module SecureHeaders
|
|
|
127
114
|
# everything else is in between.
|
|
128
115
|
BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
|
|
129
116
|
|
|
117
|
+
# These are directives that do not inherit the default-src value. This is
|
|
118
|
+
# useful when calling #combine_policies.
|
|
119
|
+
NON_FETCH_SOURCES = [
|
|
120
|
+
BASE_URI,
|
|
121
|
+
FORM_ACTION,
|
|
122
|
+
FRAME_ANCESTORS,
|
|
123
|
+
PLUGIN_TYPES,
|
|
124
|
+
REPORT_URI
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES
|
|
128
|
+
|
|
130
129
|
VARIATIONS = {
|
|
131
130
|
"Chrome" => CHROME_DIRECTIVES,
|
|
132
131
|
"Opera" => CHROME_DIRECTIVES,
|
|
@@ -156,13 +155,13 @@ module SecureHeaders
|
|
|
156
155
|
PLUGIN_TYPES => :source_list,
|
|
157
156
|
REFLECTED_XSS => :string,
|
|
158
157
|
REPORT_URI => :source_list,
|
|
159
|
-
SANDBOX => :
|
|
158
|
+
SANDBOX => :source_list,
|
|
160
159
|
SCRIPT_SRC => :source_list,
|
|
161
160
|
STYLE_SRC => :source_list,
|
|
162
161
|
UPGRADE_INSECURE_REQUESTS => :boolean
|
|
163
162
|
}.freeze
|
|
164
163
|
|
|
165
|
-
|
|
164
|
+
|
|
166
165
|
STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
|
|
167
166
|
HTTP_SCHEME_REGEX = %r{\Ahttps?://}
|
|
168
167
|
|
|
@@ -179,6 +178,11 @@ module SecureHeaders
|
|
|
179
178
|
:preserve_schemes
|
|
180
179
|
].freeze
|
|
181
180
|
|
|
181
|
+
NONCES = [
|
|
182
|
+
:script_nonce,
|
|
183
|
+
:style_nonce
|
|
184
|
+
].freeze
|
|
185
|
+
|
|
182
186
|
module ClassMethods
|
|
183
187
|
# Public: generate a header name, value array that is user-agent-aware.
|
|
184
188
|
#
|
|
@@ -194,9 +198,11 @@ module SecureHeaders
|
|
|
194
198
|
# Does not validate the invididual values of the source expression (e.g.
|
|
195
199
|
# script_src => h*t*t*p: will not raise an exception)
|
|
196
200
|
def validate_config!(config)
|
|
197
|
-
return if config.nil? || config
|
|
198
|
-
raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config
|
|
199
|
-
|
|
201
|
+
return if config.nil? || config.opt_out?
|
|
202
|
+
raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src)
|
|
203
|
+
ContentSecurityPolicyConfig.attrs.each do |key|
|
|
204
|
+
value = config.directive_value(key)
|
|
205
|
+
next unless value
|
|
200
206
|
if META_CONFIGS.include?(key)
|
|
201
207
|
raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
|
|
202
208
|
else
|
|
@@ -205,16 +211,13 @@ module SecureHeaders
|
|
|
205
211
|
end
|
|
206
212
|
end
|
|
207
213
|
|
|
208
|
-
# Public:
|
|
209
|
-
# actual value of the config.
|
|
214
|
+
# Public: check if a user agent supports CSP nonces
|
|
210
215
|
#
|
|
211
|
-
#
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return false if config == OPT_OUT
|
|
217
|
-
config == combine_policies(config, additions)
|
|
216
|
+
# user_agent - a String or a UserAgent object
|
|
217
|
+
def nonces_supported?(user_agent)
|
|
218
|
+
user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String)
|
|
219
|
+
MODERN_BROWSERS.include?(user_agent.browser) ||
|
|
220
|
+
user_agent.browser == "Safari" && user_agent.version >= CSP::VERSION_10
|
|
218
221
|
end
|
|
219
222
|
|
|
220
223
|
# Public: combine the values from two different configs.
|
|
@@ -231,7 +234,7 @@ module SecureHeaders
|
|
|
231
234
|
# 3. if a value in additions does exist in the original config, the two
|
|
232
235
|
# values are joined.
|
|
233
236
|
def combine_policies(original, additions)
|
|
234
|
-
if original ==
|
|
237
|
+
if original == {}
|
|
235
238
|
raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
|
|
236
239
|
end
|
|
237
240
|
|
|
@@ -268,8 +271,23 @@ module SecureHeaders
|
|
|
268
271
|
def populate_fetch_source_with_default!(original, additions)
|
|
269
272
|
# in case we would be appending to an empty directive, fill it with the default-src value
|
|
270
273
|
additions.keys.each do |directive|
|
|
271
|
-
|
|
272
|
-
original
|
|
274
|
+
if !original[directive] && ((source_list?(directive) && FETCH_SOURCES.include?(directive)) || nonce_added?(original, additions))
|
|
275
|
+
if nonce_added?(original, additions)
|
|
276
|
+
inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym
|
|
277
|
+
unless original[inferred_directive] || NON_FETCH_SOURCES.include?(inferred_directive)
|
|
278
|
+
original[inferred_directive] = original[:default_src]
|
|
279
|
+
end
|
|
280
|
+
else
|
|
281
|
+
original[directive] = original[:default_src]
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def nonce_added?(original, additions)
|
|
288
|
+
[:script_nonce, :style_nonce].each do |nonce|
|
|
289
|
+
if additions[nonce] && !original[nonce]
|
|
290
|
+
return true
|
|
273
291
|
end
|
|
274
292
|
end
|
|
275
293
|
end
|
|
@@ -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.
|
data/lib/secure_headers.rb
CHANGED
|
@@ -14,18 +14,44 @@ require "secure_headers/middleware"
|
|
|
14
14
|
require "secure_headers/railtie"
|
|
15
15
|
require "secure_headers/view_helper"
|
|
16
16
|
require "useragent"
|
|
17
|
+
require "singleton"
|
|
17
18
|
|
|
18
19
|
# All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
|
|
19
20
|
# or ":optout_of_protection" as a config value to disable a given header
|
|
20
21
|
module SecureHeaders
|
|
21
|
-
|
|
22
|
+
class NoOpHeaderConfig
|
|
23
|
+
include Singleton
|
|
24
|
+
|
|
25
|
+
def boom(arg = nil)
|
|
26
|
+
raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead."
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
{}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def dup
|
|
34
|
+
self.class.instance
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def opt_out?
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
alias_method :[], :boom
|
|
42
|
+
alias_method :[]=, :boom
|
|
43
|
+
alias_method :keys, :boom
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
OPT_OUT = NoOpHeaderConfig.instance
|
|
22
47
|
SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze
|
|
23
48
|
NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze
|
|
24
49
|
HTTPS = "https".freeze
|
|
25
50
|
CSP = ContentSecurityPolicy
|
|
26
51
|
|
|
27
52
|
ALL_HEADER_CLASSES = [
|
|
28
|
-
|
|
53
|
+
ContentSecurityPolicyConfig,
|
|
54
|
+
ContentSecurityPolicyReportOnlyConfig,
|
|
29
55
|
StrictTransportSecurity,
|
|
30
56
|
PublicKeyPins,
|
|
31
57
|
ReferrerPolicy,
|
|
@@ -36,7 +62,10 @@ module SecureHeaders
|
|
|
36
62
|
XXssProtection
|
|
37
63
|
].freeze
|
|
38
64
|
|
|
39
|
-
ALL_HEADERS_BESIDES_CSP = (
|
|
65
|
+
ALL_HEADERS_BESIDES_CSP = (
|
|
66
|
+
ALL_HEADER_CLASSES -
|
|
67
|
+
[ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig]
|
|
68
|
+
).freeze
|
|
40
69
|
|
|
41
70
|
# Headers set on http requests (excludes STS and HPKP)
|
|
42
71
|
HTTP_HEADER_CLASSES =
|
|
@@ -50,13 +79,25 @@ module SecureHeaders
|
|
|
50
79
|
#
|
|
51
80
|
# additions - a hash containing directives. e.g.
|
|
52
81
|
# script_src: %w(another-host.com)
|
|
53
|
-
def override_content_security_policy_directives(request, additions)
|
|
54
|
-
config =
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
def override_content_security_policy_directives(request, additions, target = nil)
|
|
83
|
+
config, target = config_and_target(request, target)
|
|
84
|
+
|
|
85
|
+
if [:both, :enforced].include?(target)
|
|
86
|
+
if config.csp.opt_out?
|
|
87
|
+
config.csp = ContentSecurityPolicyConfig.new({})
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
config.csp.merge!(additions)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if [:both, :report_only].include?(target)
|
|
94
|
+
if config.csp_report_only.opt_out?
|
|
95
|
+
config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({})
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
config.csp_report_only.merge!(additions)
|
|
57
99
|
end
|
|
58
100
|
|
|
59
|
-
config.dynamic_csp = config.current_csp.merge(additions)
|
|
60
101
|
override_secure_headers_request_config(request, config)
|
|
61
102
|
end
|
|
62
103
|
|
|
@@ -66,12 +107,25 @@ module SecureHeaders
|
|
|
66
107
|
#
|
|
67
108
|
# additions - a hash containing directives. e.g.
|
|
68
109
|
# script_src: %w(another-host.com)
|
|
69
|
-
def append_content_security_policy_directives(request, additions)
|
|
70
|
-
config =
|
|
71
|
-
|
|
110
|
+
def append_content_security_policy_directives(request, additions, target = nil)
|
|
111
|
+
config, target = config_and_target(request, target)
|
|
112
|
+
|
|
113
|
+
if [:both, :enforced].include?(target) && !config.csp.opt_out?
|
|
114
|
+
config.csp.append(additions)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if [:both, :report_only].include?(target) && !config.csp_report_only.opt_out?
|
|
118
|
+
config.csp_report_only.append(additions)
|
|
119
|
+
end
|
|
120
|
+
|
|
72
121
|
override_secure_headers_request_config(request, config)
|
|
73
122
|
end
|
|
74
123
|
|
|
124
|
+
def use_content_security_policy_named_append(request, name)
|
|
125
|
+
additions = SecureHeaders::Configuration.named_appends(name).call(request)
|
|
126
|
+
append_content_security_policy_directives(request, additions)
|
|
127
|
+
end
|
|
128
|
+
|
|
75
129
|
# Public: override X-Frame-Options settings for this request.
|
|
76
130
|
#
|
|
77
131
|
# value - deny, sameorigin, or allowall
|
|
@@ -107,12 +161,28 @@ module SecureHeaders
|
|
|
107
161
|
# returned is meant to be merged into the header value from `@app.call(env)`
|
|
108
162
|
# in Rack middleware.
|
|
109
163
|
def header_hash_for(request)
|
|
110
|
-
config = config_for(request)
|
|
111
|
-
|
|
112
|
-
|
|
164
|
+
config = config_for(request, prevent_dup = true)
|
|
165
|
+
headers = config.cached_headers
|
|
166
|
+
user_agent = UserAgent.parse(request.user_agent)
|
|
167
|
+
|
|
168
|
+
if !config.csp.opt_out? && config.csp.modified?
|
|
169
|
+
headers = update_cached_csp(config.csp, headers, user_agent)
|
|
113
170
|
end
|
|
114
171
|
|
|
115
|
-
|
|
172
|
+
if !config.csp_report_only.opt_out? && config.csp_report_only.modified?
|
|
173
|
+
headers = update_cached_csp(config.csp_report_only, headers, user_agent)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
header_classes_for(request).each_with_object({}) do |klass, hash|
|
|
177
|
+
if header = headers[klass::CONFIG_KEY]
|
|
178
|
+
header_name, value = if [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig].include?(klass)
|
|
179
|
+
csp_header_for_ua(header, user_agent)
|
|
180
|
+
else
|
|
181
|
+
header
|
|
182
|
+
end
|
|
183
|
+
hash[header_name] = value
|
|
184
|
+
end
|
|
185
|
+
end
|
|
116
186
|
end
|
|
117
187
|
|
|
118
188
|
# Public: specify which named override will be used for this request.
|
|
@@ -133,7 +203,7 @@ module SecureHeaders
|
|
|
133
203
|
#
|
|
134
204
|
# Returns the nonce
|
|
135
205
|
def content_security_policy_script_nonce(request)
|
|
136
|
-
content_security_policy_nonce(request,
|
|
206
|
+
content_security_policy_nonce(request, ContentSecurityPolicy::SCRIPT_SRC)
|
|
137
207
|
end
|
|
138
208
|
|
|
139
209
|
# Public: gets or creates a nonce for CSP.
|
|
@@ -142,7 +212,7 @@ module SecureHeaders
|
|
|
142
212
|
#
|
|
143
213
|
# Returns the nonce
|
|
144
214
|
def content_security_policy_style_nonce(request)
|
|
145
|
-
content_security_policy_nonce(request,
|
|
215
|
+
content_security_policy_nonce(request, ContentSecurityPolicy::STYLE_SRC)
|
|
146
216
|
end
|
|
147
217
|
|
|
148
218
|
# Public: Retreives the config for a given header type:
|
|
@@ -150,11 +220,15 @@ module SecureHeaders
|
|
|
150
220
|
# Checks to see if there is an override for this request, then
|
|
151
221
|
# Checks to see if a named override is used for this request, then
|
|
152
222
|
# Falls back to the global config
|
|
153
|
-
def config_for(request)
|
|
223
|
+
def config_for(request, prevent_dup = false)
|
|
154
224
|
config = request.env[SECURE_HEADERS_CONFIG] ||
|
|
155
225
|
Configuration.get(Configuration::DEFAULT_CONFIG)
|
|
156
226
|
|
|
157
|
-
|
|
227
|
+
|
|
228
|
+
# Global configs are frozen, per-request configs are not. When we're not
|
|
229
|
+
# making modifications to the config, prevent_dup ensures we don't dup
|
|
230
|
+
# the object unnecessarily. It's not necessarily frozen to begin with.
|
|
231
|
+
if config.frozen? && !prevent_dup
|
|
158
232
|
config.dup
|
|
159
233
|
else
|
|
160
234
|
config
|
|
@@ -162,13 +236,38 @@ module SecureHeaders
|
|
|
162
236
|
end
|
|
163
237
|
|
|
164
238
|
private
|
|
239
|
+
TARGETS = [:both, :enforced, :report_only]
|
|
240
|
+
def raise_on_unknown_target(target)
|
|
241
|
+
unless TARGETS.include?(target)
|
|
242
|
+
raise "Unrecognized target: #{target}. Must be [:both, :enforced, :report_only]"
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def config_and_target(request, target)
|
|
247
|
+
config = config_for(request)
|
|
248
|
+
target = guess_target(config) unless target
|
|
249
|
+
raise_on_unknown_target(target)
|
|
250
|
+
[config, target]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def guess_target(config)
|
|
254
|
+
if !config.csp.opt_out? && !config.csp_report_only.opt_out?
|
|
255
|
+
:both
|
|
256
|
+
elsif !config.csp.opt_out?
|
|
257
|
+
:enforced
|
|
258
|
+
elsif !config.csp_report_only.opt_out?
|
|
259
|
+
:report_only
|
|
260
|
+
else
|
|
261
|
+
:both
|
|
262
|
+
end
|
|
263
|
+
end
|
|
165
264
|
|
|
166
265
|
# Private: gets or creates a nonce for CSP.
|
|
167
266
|
#
|
|
168
267
|
# Returns the nonce
|
|
169
268
|
def content_security_policy_nonce(request, script_or_style)
|
|
170
269
|
request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp
|
|
171
|
-
nonce_key = script_or_style ==
|
|
270
|
+
nonce_key = script_or_style == ContentSecurityPolicy::SCRIPT_SRC ? :script_nonce : :style_nonce
|
|
172
271
|
append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY])
|
|
173
272
|
request.env[NONCE_KEY]
|
|
174
273
|
end
|
|
@@ -193,21 +292,12 @@ module SecureHeaders
|
|
|
193
292
|
end
|
|
194
293
|
end
|
|
195
294
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if header = headers[klass::CONFIG_KEY]
|
|
203
|
-
header_name, value = if klass == CSP
|
|
204
|
-
csp_header_for_ua(header, request)
|
|
205
|
-
else
|
|
206
|
-
header
|
|
207
|
-
end
|
|
208
|
-
hash[header_name] = value
|
|
209
|
-
end
|
|
210
|
-
end
|
|
295
|
+
def update_cached_csp(config, headers, user_agent)
|
|
296
|
+
headers = Configuration.send(:deep_copy, headers)
|
|
297
|
+
headers[config.class::CONFIG_KEY] = {}
|
|
298
|
+
variation = ContentSecurityPolicy.ua_to_variation(user_agent)
|
|
299
|
+
headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent)
|
|
300
|
+
headers
|
|
211
301
|
end
|
|
212
302
|
|
|
213
303
|
# Private: chooses the applicable CSP header for the provided user agent.
|
|
@@ -215,26 +305,8 @@ module SecureHeaders
|
|
|
215
305
|
# headers - a hash of header_config_key => [header_name, header_value]
|
|
216
306
|
#
|
|
217
307
|
# Returns a CSP [header, value] array
|
|
218
|
-
def csp_header_for_ua(headers,
|
|
219
|
-
headers[
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# Private: optionally build a header with a given configure
|
|
223
|
-
#
|
|
224
|
-
# klass - corresponding Class for a given header
|
|
225
|
-
# config - A string, symbol, or hash config for the header
|
|
226
|
-
# user_agent - A string representing the UA (only used for CSP feature sniffing)
|
|
227
|
-
#
|
|
228
|
-
# Returns a 2 element array [header_name, header_value] or nil if config
|
|
229
|
-
# is OPT_OUT
|
|
230
|
-
def make_header(klass, header_config, user_agent = nil)
|
|
231
|
-
unless header_config == OPT_OUT
|
|
232
|
-
if klass == CSP
|
|
233
|
-
klass.make_header(header_config, user_agent)
|
|
234
|
-
else
|
|
235
|
-
klass.make_header(header_config)
|
|
236
|
-
end
|
|
237
|
-
end
|
|
308
|
+
def csp_header_for_ua(headers, user_agent)
|
|
309
|
+
headers[ContentSecurityPolicy.ua_to_variation(user_agent)]
|
|
238
310
|
end
|
|
239
311
|
end
|
|
240
312
|
|
|
@@ -267,4 +339,8 @@ module SecureHeaders
|
|
|
267
339
|
def override_x_frame_options(value)
|
|
268
340
|
SecureHeaders.override_x_frame_options(request, value)
|
|
269
341
|
end
|
|
342
|
+
|
|
343
|
+
def use_content_security_policy_named_append(name)
|
|
344
|
+
SecureHeaders.use_content_security_policy_named_append(request, name)
|
|
345
|
+
end
|
|
270
346
|
end
|
data/secure_headers.gemspec
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.5.0"
|
|
5
5
|
gem.authors = ["Neil Matatall"]
|
|
6
6
|
gem.email = ["neil.matatall@gmail.com"]
|
|
7
7
|
gem.description = 'Security related headers all in one gem.'
|
|
@@ -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
|
|
@@ -14,11 +14,11 @@ module SecureHeaders
|
|
|
14
14
|
|
|
15
15
|
describe "#name" do
|
|
16
16
|
context "when in report-only mode" do
|
|
17
|
-
specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(
|
|
17
|
+
specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME) }
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
context "when in enforce mode" do
|
|
21
|
-
specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(
|
|
21
|
+
specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(ContentSecurityPolicyConfig::HEADER_NAME) }
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|